diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..740f292 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,24 @@ +[submodule "docs/routers/FredM67-PVRouter-1-phase"] + path = docs/routers/FredM67-PVRouter-1-phase + url = git@github.com:mathieucarbou/FredM67-PVRouter-1-phase.git +[submodule "docs/routers/FlaredM67-PVRouter-3-phase"] + path = docs/routers/FredM67-PVRouter-3-phase + url = git@github.com:mathieucarbou/FredM67-PVRouter-3-phase.git +[submodule "docs/routers/xlyric-pv-router-esp32"] + path = docs/routers/xlyric-pv-router-esp32 + url = git@github.com:mathieucarbou/xlyric-pv-router-esp32.git +[submodule "docs/routers/xlyric-PV-discharge-Dimmer-AC-Dimmer-KIT-Robotdyn"] + path = docs/routers/xlyric-PV-discharge-Dimmer-AC-Dimmer-KIT-Robotdyn + url = git@github.com:mathieucarbou/xlyric-PV-discharge-Dimmer-AC-Dimmer-KIT-Robotdyn.git +[submodule "docs/routers/routeur_le_professolaire"] + path = docs/routers/routeur_le_professolaire + url = git@github.com:mathieucarbou/routeur_le_professolaire.git +[submodule "docs/routers/Jetblack31-MaxPV"] + path = docs/routers/Jetblack31-MaxPV + url = git@github.com:mathieucarbou/Jetblack31-MaxPV.git +[submodule "docs/routers/FredM67-PVRouter-3-phase"] + path = docs/routers/FredM67-PVRouter-3-phase + url = git@github.com:mathieucarbou/FredM67-PVRouter-3-phase.git +[submodule "docs/routers/routeur_solaire"] + path = docs/routers/routeur_solaire + url = https://github.com/mathieucarbou/SeByDocKy-routeur_solaire diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..cde073f --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +/_site +/.jekyll-cache +/.sass-cache diff --git a/docs/CNAME b/docs/CNAME new file mode 100644 index 0000000..e13623d --- /dev/null +++ b/docs/CNAME @@ -0,0 +1 @@ +yasolr.carbou.me \ No newline at end of file diff --git a/docs/Gemfile b/docs/Gemfile new file mode 100644 index 0000000..355c167 --- /dev/null +++ b/docs/Gemfile @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gem "jekyll" + +# If you have any plugins, put them here! +group :jekyll_plugins do + gem "jekyll-remote-theme" + gem "jekyll-seo-tag" + gem "kramdown-parser-gfm" + gem "webrick" +end diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock new file mode 100644 index 0000000..6690865 --- /dev/null +++ b/docs/Gemfile.lock @@ -0,0 +1,85 @@ +GEM + remote: https://rubygems.org/ + specs: + addressable (2.8.6) + public_suffix (>= 2.0.2, < 6.0) + colorator (1.1.0) + concurrent-ruby (1.2.3) + em-websocket (0.5.3) + eventmachine (>= 0.12.9) + http_parser.rb (~> 0) + eventmachine (1.2.7) + ffi (1.16.3) + forwardable-extended (2.6.0) + google-protobuf (4.26.1-arm64-darwin) + rake (>= 13) + http_parser.rb (0.8.0) + i18n (1.14.5) + concurrent-ruby (~> 1.0) + jekyll (4.3.3) + addressable (~> 2.4) + colorator (~> 1.0) + em-websocket (~> 0.5) + i18n (~> 1.0) + jekyll-sass-converter (>= 2.0, < 4.0) + jekyll-watch (~> 2.0) + kramdown (~> 2.3, >= 2.3.1) + kramdown-parser-gfm (~> 1.0) + liquid (~> 4.0) + mercenary (>= 0.3.6, < 0.5) + pathutil (~> 0.9) + rouge (>= 3.0, < 5.0) + safe_yaml (~> 1.0) + terminal-table (>= 1.8, < 4.0) + webrick (~> 1.7) + jekyll-remote-theme (0.4.3) + addressable (~> 2.0) + jekyll (>= 3.5, < 5.0) + jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0) + rubyzip (>= 1.3.0, < 3.0) + jekyll-sass-converter (3.0.0) + sass-embedded (~> 1.54) + jekyll-seo-tag (2.8.0) + jekyll (>= 3.8, < 5.0) + jekyll-watch (2.2.1) + listen (~> 3.0) + kramdown (2.4.0) + rexml + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) + liquid (4.0.4) + listen (3.9.0) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + mercenary (0.4.0) + pathutil (0.16.2) + forwardable-extended (~> 2.6) + public_suffix (5.0.5) + rake (13.2.1) + rb-fsevent (0.11.2) + rb-inotify (0.10.1) + ffi (~> 1.0) + rexml (3.2.6) + rouge (4.2.1) + rubyzip (2.3.2) + safe_yaml (1.0.5) + sass-embedded (1.77.1-arm64-darwin) + google-protobuf (>= 3.25, < 5.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + unicode-display_width (2.5.0) + webrick (1.8.1) + +PLATFORMS + arm64-darwin-22 + arm64-darwin-23 + +DEPENDENCIES + jekyll + jekyll-remote-theme + jekyll-seo-tag + kramdown-parser-gfm + webrick + +BUNDLED WITH + 2.4.22 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..d27e6cf --- /dev/null +++ b/docs/README.md @@ -0,0 +1,4 @@ +Installation: https://jekyllrb.com/docs/installation/macos/ + +bundle install +bundle exec jekyll serve --host=0.0.0.0 diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000..bd9b357 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,8 @@ +# bundle exec jekyll serve --host=0.0.0.0 + +title: YaS☀️lR (Yet another Solar Router) +description: Heat water with your Solar Production Excess! +remote_theme: pages-themes/cayman@v0.2.0 +plugins: + - jekyll-remote-theme + \ No newline at end of file diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html new file mode 100644 index 0000000..a04683f --- /dev/null +++ b/docs/_layouts/default.html @@ -0,0 +1,116 @@ + + + + + + + {% seo %} + + + + + + + {% include head-custom.html %} + + + + Skip to the content. + + + +
+ {{ content }} + + +
+ + + + + + \ No newline at end of file diff --git a/docs/assets/css/style.scss b/docs/assets/css/style.scss new file mode 100644 index 0000000..d06c4cf --- /dev/null +++ b/docs/assets/css/style.scss @@ -0,0 +1,117 @@ +--- +--- + +// https://github.com/pages-themes/cayman + +// Breakpoints +$large-breakpoint: 64em !default; +$medium-breakpoint: 42em !default; + +// Headers +$header-heading-color: #f0f0f0 !default; +$header-bg-color: #fce23b !default; +$header-bg-color-secondary: #155799 !default; +$header-menu-bg-color: #f8c149 !default; + +// Text +$section-headings-color: #159957 !default; +$body-text-color: #3d3d3e !default; +$body-link-color: #1e6bb8 !default; +$blockquote-text-color: #819198 !default; + +// Code +$code-bg-color: #f3f6fa !default; +$code-text-color: #567482 !default; + +// Borders +$border-color: #dce6f0 !default; +$table-border-color: #e9ebec !default; +$hr-border-color: #eff0f1 !default; + +@import "{{ site.theme }}"; + +.page-header { + padding: 1rem; + font-weight: bolder; + color: $header-heading-color; + + .page-description { + font-style: italic; + font-size: larger; + } + + .project-tagline { + font-weight: inherit; + margin-bottom: 0; + font-size: 15px; + + .btn { + color: $header-heading-color; + background-color: darken($header-menu-bg-color, 20%); + border-color: $header-bg-color-secondary; + } + + .btn:hover { + box-shadow: 0 12px 16px 0 rgba(0, 0, 0, 0.24), 0 17px 50px 0 rgba(0, 0, 0, 0.19); + } + + .btn-small { + padding: 5px 10px 5px 10px; + color: $header-heading-color; + background-color: darken($header-menu-bg-color, 20%); + border-color: $header-bg-color-secondary; + } + + .btn-small + .btn-small { + //margin-left: 0.5rem; + margin-left: 0; + } + + .btn-small:hover { + box-shadow: 0 12px 16px 0 rgba(0, 0, 0, 0.24), 0 17px 50px 0 rgba(0, 0, 0, 0.19); + } + + .btn-small.active { + background-color: darken($header-bg-color, 20%); + } + } +} + +#google_translate_element { + position: absolute; + right: 1rem; +} + +.project-name { + margin: 0; + font-size: inherit; + + div { + margin-left: auto; + margin-right: auto; + width: fit-content; + background-color: #15579980; + padding: 10px; + border-radius: 15px; + } +} + +input.task-list-item-checkbox { + margin-right: 7px; +} + +.site-footer-owner { + font-weight: normal; +} + +@media screen and (max-width: 730px) { + .project-name { + div { + margin-top: 60px; + } + } +} + +.main-content { + font-size: medium; +} \ No newline at end of file diff --git a/docs/assets/img/Github_Donate.png b/docs/assets/img/Github_Donate.png new file mode 100644 index 0000000..3c1bbe4 Binary files /dev/null and b/docs/assets/img/Github_Donate.png differ diff --git a/docs/assets/img/Paypal_Donate.png b/docs/assets/img/Paypal_Donate.png new file mode 100644 index 0000000..ec3a346 Binary files /dev/null and b/docs/assets/img/Paypal_Donate.png differ diff --git a/docs/assets/img/builds/rnd_box_1.jpeg b/docs/assets/img/builds/rnd_box_1.jpeg new file mode 100644 index 0000000..9f3b7e3 Binary files /dev/null and b/docs/assets/img/builds/rnd_box_1.jpeg differ diff --git a/docs/assets/img/builds/rnd_box_2.jpeg b/docs/assets/img/builds/rnd_box_2.jpeg new file mode 100644 index 0000000..1982eea Binary files /dev/null and b/docs/assets/img/builds/rnd_box_2.jpeg differ diff --git a/docs/assets/img/hardware/BTA40-800B.jpeg b/docs/assets/img/hardware/BTA40-800B.jpeg new file mode 100644 index 0000000..864e5c9 Binary files /dev/null and b/docs/assets/img/hardware/BTA40-800B.jpeg differ diff --git a/docs/assets/img/hardware/DIN_1_Relay.jpeg b/docs/assets/img/hardware/DIN_1_Relay.jpeg new file mode 100644 index 0000000..76eae5d Binary files /dev/null and b/docs/assets/img/hardware/DIN_1_Relay.jpeg differ diff --git a/docs/assets/img/hardware/DIN_2_Relay.jpeg b/docs/assets/img/hardware/DIN_2_Relay.jpeg new file mode 100644 index 0000000..c4c35a2 Binary files /dev/null and b/docs/assets/img/hardware/DIN_2_Relay.jpeg differ diff --git a/docs/assets/img/hardware/DIN_HDR-15-5.jpeg b/docs/assets/img/hardware/DIN_HDR-15-5.jpeg new file mode 100644 index 0000000..d69b933 Binary files /dev/null and b/docs/assets/img/hardware/DIN_HDR-15-5.jpeg differ diff --git a/docs/assets/img/hardware/DIN_SSR_Clip.png b/docs/assets/img/hardware/DIN_SSR_Clip.png new file mode 100644 index 0000000..1af6c19 Binary files /dev/null and b/docs/assets/img/hardware/DIN_SSR_Clip.png differ diff --git a/docs/assets/img/hardware/DS18B20.jpeg b/docs/assets/img/hardware/DS18B20.jpeg new file mode 100644 index 0000000..cf0fa80 Binary files /dev/null and b/docs/assets/img/hardware/DS18B20.jpeg differ diff --git a/docs/assets/img/hardware/Distrib_DIN.jpeg b/docs/assets/img/hardware/Distrib_DIN.jpeg new file mode 100644 index 0000000..a3eee9a Binary files /dev/null and b/docs/assets/img/hardware/Distrib_DIN.jpeg differ diff --git a/docs/assets/img/hardware/DupontWire.jpeg b/docs/assets/img/hardware/DupontWire.jpeg new file mode 100644 index 0000000..779793b Binary files /dev/null and b/docs/assets/img/hardware/DupontWire.jpeg differ diff --git a/docs/assets/img/hardware/ESP32-S3.jpeg b/docs/assets/img/hardware/ESP32-S3.jpeg new file mode 100644 index 0000000..efac84f Binary files /dev/null and b/docs/assets/img/hardware/ESP32-S3.jpeg differ diff --git a/docs/assets/img/hardware/ESP32S_Din_Rail_Mount.jpeg b/docs/assets/img/hardware/ESP32S_Din_Rail_Mount.jpeg new file mode 100644 index 0000000..e678c1a Binary files /dev/null and b/docs/assets/img/hardware/ESP32S_Din_Rail_Mount.jpeg differ diff --git a/docs/assets/img/hardware/ESP32_NodeMCU.jpeg b/docs/assets/img/hardware/ESP32_NodeMCU.jpeg new file mode 100644 index 0000000..365d82b Binary files /dev/null and b/docs/assets/img/hardware/ESP32_NodeMCU.jpeg differ diff --git a/docs/assets/img/hardware/Electric_Box.jpeg b/docs/assets/img/hardware/Electric_Box.jpeg new file mode 100644 index 0000000..d33731a Binary files /dev/null and b/docs/assets/img/hardware/Electric_Box.jpeg differ diff --git a/docs/assets/img/hardware/Extension_Board.jpeg b/docs/assets/img/hardware/Extension_Board.jpeg new file mode 100644 index 0000000..d02c0c9 Binary files /dev/null and b/docs/assets/img/hardware/Extension_Board.jpeg differ diff --git a/docs/assets/img/hardware/Heat_Sink.jpeg b/docs/assets/img/hardware/Heat_Sink.jpeg new file mode 100644 index 0000000..967870b Binary files /dev/null and b/docs/assets/img/hardware/Heat_Sink.jpeg differ diff --git a/docs/assets/img/hardware/JSY-MK-194T_1.jpeg b/docs/assets/img/hardware/JSY-MK-194T_1.jpeg new file mode 100644 index 0000000..1998795 Binary files /dev/null and b/docs/assets/img/hardware/JSY-MK-194T_1.jpeg differ diff --git a/docs/assets/img/hardware/JSY-MK-194T_2.jpeg b/docs/assets/img/hardware/JSY-MK-194T_2.jpeg new file mode 100644 index 0000000..1802ba1 Binary files /dev/null and b/docs/assets/img/hardware/JSY-MK-194T_2.jpeg differ diff --git a/docs/assets/img/hardware/LCTC_Voltage_Regulator_220V_40A.jpeg b/docs/assets/img/hardware/LCTC_Voltage_Regulator_220V_40A.jpeg new file mode 100644 index 0000000..10e315e Binary files /dev/null and b/docs/assets/img/hardware/LCTC_Voltage_Regulator_220V_40A.jpeg differ diff --git a/docs/assets/img/hardware/LCTC_Voltage_Regulator_DTY-220V40P1.jpeg b/docs/assets/img/hardware/LCTC_Voltage_Regulator_DTY-220V40P1.jpeg new file mode 100644 index 0000000..295cd8d Binary files /dev/null and b/docs/assets/img/hardware/LCTC_Voltage_Regulator_DTY-220V40P1.jpeg differ diff --git a/docs/assets/img/hardware/LEDs.jpeg b/docs/assets/img/hardware/LEDs.jpeg new file mode 100644 index 0000000..dbd8d3e Binary files /dev/null and b/docs/assets/img/hardware/LEDs.jpeg differ diff --git a/docs/assets/img/hardware/LILYGO-T-ETH-Lite.jpeg b/docs/assets/img/hardware/LILYGO-T-ETH-Lite.jpeg new file mode 100644 index 0000000..f7cc397 Binary files /dev/null and b/docs/assets/img/hardware/LILYGO-T-ETH-Lite.jpeg differ diff --git a/docs/assets/img/hardware/LSA-H3P50YB.jpeg b/docs/assets/img/hardware/LSA-H3P50YB.jpeg new file mode 100644 index 0000000..9e63118 Binary files /dev/null and b/docs/assets/img/hardware/LSA-H3P50YB.jpeg differ diff --git a/docs/assets/img/hardware/Nodemcu-ESP-32S.jpeg b/docs/assets/img/hardware/Nodemcu-ESP-32S.jpeg new file mode 100644 index 0000000..f70ca79 Binary files /dev/null and b/docs/assets/img/hardware/Nodemcu-ESP-32S.jpeg differ diff --git a/docs/assets/img/hardware/PWM_33_0-10.jpeg b/docs/assets/img/hardware/PWM_33_0-10.jpeg new file mode 100644 index 0000000..ad6caaa Binary files /dev/null and b/docs/assets/img/hardware/PWM_33_0-10.jpeg differ diff --git a/docs/assets/img/hardware/PZEM-004T.jpeg b/docs/assets/img/hardware/PZEM-004T.jpeg new file mode 100644 index 0000000..3b62a39 Binary files /dev/null and b/docs/assets/img/hardware/PZEM-004T.jpeg differ diff --git a/docs/assets/img/hardware/Passive_Buzzer.jpeg b/docs/assets/img/hardware/Passive_Buzzer.jpeg new file mode 100644 index 0000000..138d1ca Binary files /dev/null and b/docs/assets/img/hardware/Passive_Buzzer.jpeg differ diff --git a/docs/assets/img/hardware/Pigtail_Antenna.jpeg b/docs/assets/img/hardware/Pigtail_Antenna.jpeg new file mode 100644 index 0000000..b94edb6 Binary files /dev/null and b/docs/assets/img/hardware/Pigtail_Antenna.jpeg differ diff --git a/docs/assets/img/hardware/PushButton.jpeg b/docs/assets/img/hardware/PushButton.jpeg new file mode 100644 index 0000000..699ad58 Binary files /dev/null and b/docs/assets/img/hardware/PushButton.jpeg differ diff --git a/docs/assets/img/hardware/RC_Snubber.jpeg b/docs/assets/img/hardware/RC_Snubber.jpeg new file mode 100644 index 0000000..0f70aa0 Binary files /dev/null and b/docs/assets/img/hardware/RC_Snubber.jpeg differ diff --git a/docs/assets/img/hardware/Random_SSR.jpeg b/docs/assets/img/hardware/Random_SSR.jpeg new file mode 100644 index 0000000..ff23f68 Binary files /dev/null and b/docs/assets/img/hardware/Random_SSR.jpeg differ diff --git a/docs/assets/img/hardware/Random_SSR_EARU.jpeg b/docs/assets/img/hardware/Random_SSR_EARU.jpeg new file mode 100644 index 0000000..cfee30b Binary files /dev/null and b/docs/assets/img/hardware/Random_SSR_EARU.jpeg differ diff --git a/docs/assets/img/hardware/Raspberry_Fans.jpeg b/docs/assets/img/hardware/Raspberry_Fans.jpeg new file mode 100644 index 0000000..d297197 Binary files /dev/null and b/docs/assets/img/hardware/Raspberry_Fans.jpeg differ diff --git a/docs/assets/img/hardware/Robodyn_24A.jpeg b/docs/assets/img/hardware/Robodyn_24A.jpeg new file mode 100644 index 0000000..b3ef121 Binary files /dev/null and b/docs/assets/img/hardware/Robodyn_24A.jpeg differ diff --git a/docs/assets/img/hardware/Robodyn_40A.jpeg b/docs/assets/img/hardware/Robodyn_40A.jpeg new file mode 100644 index 0000000..bde1192 Binary files /dev/null and b/docs/assets/img/hardware/Robodyn_40A.jpeg differ diff --git a/docs/assets/img/hardware/SH1106.jpeg b/docs/assets/img/hardware/SH1106.jpeg new file mode 100644 index 0000000..5f39613 Binary files /dev/null and b/docs/assets/img/hardware/SH1106.jpeg differ diff --git a/docs/assets/img/hardware/SH1107.jpeg b/docs/assets/img/hardware/SH1107.jpeg new file mode 100644 index 0000000..1009859 Binary files /dev/null and b/docs/assets/img/hardware/SH1107.jpeg differ diff --git a/docs/assets/img/hardware/SSD1306.jpeg b/docs/assets/img/hardware/SSD1306.jpeg new file mode 100644 index 0000000..9d627c0 Binary files /dev/null and b/docs/assets/img/hardware/SSD1306.jpeg differ diff --git a/docs/assets/img/hardware/SSR_40A_DA.jpeg b/docs/assets/img/hardware/SSR_40A_DA.jpeg new file mode 100644 index 0000000..661f3e1 Binary files /dev/null and b/docs/assets/img/hardware/SSR_40A_DA.jpeg differ diff --git a/docs/assets/img/hardware/SSR_Heat_Sink.png b/docs/assets/img/hardware/SSR_Heat_Sink.png new file mode 100644 index 0000000..ea3b151 Binary files /dev/null and b/docs/assets/img/hardware/SSR_Heat_Sink.png differ diff --git a/docs/assets/img/hardware/Shelly_Addon.jpeg b/docs/assets/img/hardware/Shelly_Addon.jpeg new file mode 100644 index 0000000..8562860 Binary files /dev/null and b/docs/assets/img/hardware/Shelly_Addon.jpeg differ diff --git a/docs/assets/img/hardware/Shelly_Addon_DS18.jpeg b/docs/assets/img/hardware/Shelly_Addon_DS18.jpeg new file mode 100644 index 0000000..c8119a7 Binary files /dev/null and b/docs/assets/img/hardware/Shelly_Addon_DS18.jpeg differ diff --git a/docs/assets/img/hardware/Shelly_DS18.jpeg b/docs/assets/img/hardware/Shelly_DS18.jpeg new file mode 100644 index 0000000..d6be3cc Binary files /dev/null and b/docs/assets/img/hardware/Shelly_DS18.jpeg differ diff --git a/docs/assets/img/hardware/Shelly_Dimmer-10V.jpeg b/docs/assets/img/hardware/Shelly_Dimmer-10V.jpeg new file mode 100644 index 0000000..20899d2 Binary files /dev/null and b/docs/assets/img/hardware/Shelly_Dimmer-10V.jpeg differ diff --git a/docs/assets/img/hardware/Shelly_EM.png b/docs/assets/img/hardware/Shelly_EM.png new file mode 100644 index 0000000..1d15757 Binary files /dev/null and b/docs/assets/img/hardware/Shelly_EM.png differ diff --git a/docs/assets/img/hardware/Shelly_Pro_EM_50.jpeg b/docs/assets/img/hardware/Shelly_Pro_EM_50.jpeg new file mode 100644 index 0000000..8c84c60 Binary files /dev/null and b/docs/assets/img/hardware/Shelly_Pro_EM_50.jpeg differ diff --git a/docs/assets/img/hardware/WT32-ETH01.jpeg b/docs/assets/img/hardware/WT32-ETH01.jpeg new file mode 100644 index 0000000..ce5efdc Binary files /dev/null and b/docs/assets/img/hardware/WT32-ETH01.jpeg differ diff --git a/docs/assets/img/hardware/ZCD.jpeg b/docs/assets/img/hardware/ZCD.jpeg new file mode 100644 index 0000000..5aeafc3 Binary files /dev/null and b/docs/assets/img/hardware/ZCD.jpeg differ diff --git a/docs/assets/img/hardware/ZCD_DIN_Rail.jpeg b/docs/assets/img/hardware/ZCD_DIN_Rail.jpeg new file mode 100644 index 0000000..4c19db2 Binary files /dev/null and b/docs/assets/img/hardware/ZCD_DIN_Rail.jpeg differ diff --git a/docs/assets/img/hardware/jsy-enclosure.jpeg b/docs/assets/img/hardware/jsy-enclosure.jpeg new file mode 100644 index 0000000..5021fc8 Binary files /dev/null and b/docs/assets/img/hardware/jsy-enclosure.jpeg differ diff --git a/docs/assets/img/hardware/shelly_solar_diverter_poc.jpeg b/docs/assets/img/hardware/shelly_solar_diverter_poc.jpeg new file mode 100644 index 0000000..31b0b05 Binary files /dev/null and b/docs/assets/img/hardware/shelly_solar_diverter_poc.jpeg differ diff --git a/docs/assets/img/logo-60px.png b/docs/assets/img/logo-60px.png new file mode 100644 index 0000000..a2f48bb Binary files /dev/null and b/docs/assets/img/logo-60px.png differ diff --git "a/docs/assets/img/logo-640\303\227320px.png" "b/docs/assets/img/logo-640\303\227320px.png" new file mode 100644 index 0000000..1abaebe Binary files /dev/null and "b/docs/assets/img/logo-640\303\227320px.png" differ diff --git a/docs/assets/img/logo-big.png b/docs/assets/img/logo-big.png new file mode 100644 index 0000000..1dfe3c4 Binary files /dev/null and b/docs/assets/img/logo-big.png differ diff --git a/docs/assets/img/logo.png b/docs/assets/img/logo.png new file mode 100644 index 0000000..c5785cb Binary files /dev/null and b/docs/assets/img/logo.png differ diff --git a/docs/assets/img/measurements/20240403162331.set b/docs/assets/img/measurements/20240403162331.set new file mode 100644 index 0000000..be3ae8e --- /dev/null +++ b/docs/assets/img/measurements/20240403162331.set @@ -0,0 +1,326 @@ +#Wed Apr 03 16:23:34 CEST 2024 +SingleTrg.LIN.dlc=0 +Zoom.middleofborders=0 +CH2.Edge.raisefall=0 +CH1.Video.syncValue=1 +Bus1.CAN.source=0 +CH2.probeMultiIdx=1 +SingleTrg.CAN.baudCustom=10000.0 +NetWork.setSubnetmask=255.255.255.0 +CH1.on=1 +CH2.Slope.condition=0 +CH2.Slope.sweep=0 +CH2.amperePerVolt=10000 +CH1.measureCurrentOn=0 +triggerChannelIndex=0 +CH1.Pulse.coupling=0 +Zoom.onoff=0 +Bus1.LIN.signal=0 +CH2.Slope.uppest=1 +CH1.probeMultiIdx=2 +Bus1.protocol=0 +CH4.Edge.holdoff=1 +CH1.Pulse.sweep=0 +Bus2.display=0 +SingleTrg.holdoff_v=1 +CH1.amperePerVolt=10000 +SingleTrg.Edge.coupling=1 +NetWork.setMac=20.127.15.207.245.4 +SingleTrg.Slope.sweep=0 +CH1.forcebandlimit=0 +MarkCursor.y2=360 +ComputeFreqTimes=5 +CH3.couplingIdx=1 +MarkCursor.y1=142 +CH1.pos0=0 +SingleTrg.Video.syncValue=0 +SingleTrg.CAN.level=0 +Bus2.LIN.baudrate=2400.0 +SingleTrg.CAN.type=0 +CH4.Pulse.argument=3 +Sample.mode=0 +Country= +Math.ffton=0 +SingleTrg.CAN.baud=0 +SingleTrg.trgMode=0 +CH4.Edge.coupling=0 +SingleTrg.Pulse.holdoff=1 +CH1.Slope.condition=0 +RecordIntervalTime=40 +CH4.Pulse.voltsense=0 +MarkCursor.x2=662 +MarkCursor.x1=362 +MeasureDelayCode= +Sine.amplitude=1.0 +CH3.Edge.voltsense=0 +CH4.inverse=0 +Pulse.dutyCycle=50.0 +lowMachineChannels=2 +selectedwfIdx=0 +Rule.current=1,0.2,0.2 +Bus2.CAN.brtype=4 +SingleTrg.LIN.voltsense=0 +CH2.bandlimit=0 +SingleTrg.Slope.lowest=75 +CH2.on=1 +CH4.Slope.uppest=1 +CH3.Edge.coupling=0 +Bus2.CAN.samplePoint=50.0 +CH4.Pulse.condition=0 +ExportPath=/Users/mat/Downloads +Rule.msg=0 +Bus2.LIN.source=0 +FFTCursor.onFrebaseMark=0 +CH4.vbIdx=6 +CH4.Video.sync=0 +Pulse.offset=0.0 +CH1.Pulse.argument=3 +bus.thresholds.4=0 +bus.thresholds.3=0 +bus.thresholds.2=0 +bus.thresholds.1=0 +FGen.selection=0 +CH3.Pulse.coupling=0 +Zoom.tbz=16 +CH4.trgMode=0 +CH1.Edge.holdoff=1 +CH3.Pulse.holdoff=1 +CH2.Edge.coupling=0 +Bus1.LIN.brtype=2 +SingleTrg.Slope.condition=1 +CH1.Slope.argument=3 +Ramp.symmetry=50.0 +SaveimgPath=/Users/mat/Downloads +NetWork.linkIP=192.168.1.172 +CH3.Pulse.voltsense=0 +Tune.tid=0 +Bus1.CAN.signal=0 +CH1.Edge.coupling=0 +CH2.Pulse.holdoff=1 +CH3.Slope.sweep=0 +CH4.measureCurrentOn=1 +CH4.Slope.holdoff=1 +CH3.vbIdx=10 +SingleTrg.CAN.id=0 +Bus1.CAN.samplePoint=50.0 +Ramp.amplitude=1.0 +SingleTrg.Video.module=0 +SingleTrg.Video.sync=0 +CH2.inverse=0 +NetWork.setGateway=192.168.8.1 +SingleTrg.CAN.when=0 +CH2.Pulse.sweep=0 +Math.operation=0 +CH1.Pulse.holdoff=1 +Ramp.frequency=1000.0 +CH3.Edge.sweep=0 +CH3.on=1 +Bus1.LIN.samplePoint=50.0 +Timebase.index=18 +CH3.Slope.holdoff=1 +CH1.Slope.uppest=1 +CH4.Video.syncValue=1 +RecordPath=wave1.cap +SingleTrg.Edge.voltsense=0 +DeepMemory=4 +CH4.couplingIdx=0 +LineLink=1 +CH3.Video.module=0 +CH2.Video.sync=0 +CH2.Pulse.voltsense=0 +Math.m2=1 +NetWork.setPort=8866 +STYLE_TYPE=0 +Math.m1=0 +CH2.trgMode=0 +CustomizePages=1,6,3,8,11 +CH4.Video.holdoff=1 +Language=en +SingleTrg.Pulse.sweep=0 +Bus1.LIN.baudrate=2400.0 +MarkCursor.CHNum=0 +SingleTrg.Pulse.coupling=1 +Bus2.CAN.source=0 +circleSerialPort=2 +ReferenceObjsUseable= +SingleTrg.holdoff_en=0 +SingleTrg.LIN.baud=0 +SingleTrg.selectIndex=0 +Bus2.CAN.baudrate=10000.0 +Print.rightEdgeLength=20 +Rule.outputRule=0 +FFTCursor.onVoltbaseMark=0 +ReferenceIsValid=1,0,0,0, +Print.isVertical=1 +CH4.Slope.argument=3 +Bus2.LIN.signal=0 +CH3.Video.syncValue=1 +SingleTrg.LIN.id=0 +CH4.Slope.condition=0 +CH2.Pulse.coupling=0 +NetWork.Sync_out=1 +SingleTrg.LIN.coupling=0 +PlayPath=wave1.cap +Rule.ring=0 +SingleTrg.CAN.data=0.0.0.0.0.0.0.0 +CH4.Edge.raisefall=0 +debug=1 +Square.frequency=1000.0 +CH4.on=1 +Sine.offset=0.0 +CH1.bandlimit=0 +Bus2.CAN=CAN +SingleTrg.LIN.stype=0 +HorTrgPos=0 +ScpiPort=5188 +Rule.stopOnOutput=0 +Pulse.amplitude=1.0 +CH2.Slope.lowest=1 +CH1.Edge.sweep=0 +CH4.bandlimit=0 +CH4.Pulse.sweep=0 +CH4.Slope.sweep=0 +Pulse.frequency=1000.0 +Ramp.offset=0.0 +SingleTrg.CAN.samplePoint=50 +SingleTrg.Slope.holdoff=1 +SingleTrg.LIN.baudCustom=10000.0 +XYModeOn=0 +triggerChannelMode=0 +CH1.Edge.raisefall=0 +Bus1.CAN=CAN +Math.fftscale=0 +MeasureTypes=17,4,13,12,11, +CH3.Pulse.sweep=0 +Rule.rules=1,0.2,0.2 ; 2,0.5,0.5 ; +CH2.Video.syncValue=1 +SingleTrg.LIN.samplePoint=50 +CH1.couplingIdx=0 +CH3.Slope.condition=0 +SingleTrg.Pulse.voltsense=0 +CH4.probeMultiIdx=0 +CH3.Pulse.condition=0 +SingleTrg.CAN.voltsense=0 +Bus2.protocol=0 +CH4.amperePerVolt=10000 +TipsWindowShow=0 +CH3.measureCurrentOn=0 +CH4.Slope.lowest=1 +Bus2.LIN.brtype=2 +SingleTrg.LIN.when=0 +RecordEndFrame=300 +Bus1.display=0 +SingleTrg.Video.holdoff=1 +CH3.Edge.holdoff=1 +CH4.rgb=CC00FF +WaveFormY=1 +WaveFormX=0 +CH2.Video.module=0 +CH1.trgMode=0 +CH1.Video.sync=0 +ReferenceObjsNames=0,0,0,0,0,0,0,0, +Print.isPaintWFBG=0 +CH3.amperePerVolt=10000 +CH4.pos0=0 +Math.fftwnd=0 +CH3.Pulse.argument=3 +maxFailureTime=20 +CH2.measureCurrentOn=0 +Bus2.CAN.signal=0 +Zoom.ztimebaseidx=8 +CH2.Slope.holdoff=1 +OpenfilePath= +CH2.vbIdx=7 +Math.fftvaluetype=1 +SingleTrg.Slope.uppest=75 +CH2.Edge.voltsense=0 +CH3.Slope.argument=3 +CH3.rgb=66CCFF +CH2.Pulse.condition=0 +Bus1.CAN.baudrate=10000.0 +CH3.inverse=0 +CH4.Edge.sweep=0 +CH1.Slope.holdoff=1 +CH3.Edge.raisefall=0 +NetWork.setIP=192.168.8.172 +CH3.Video.holdoff=1 +dbgbtns=0 +PersistenceIndex=0 +Bus1.CAN.brtype=4 +RecordCounter=0 +Square.amplitude=1.0 +CH3.Slope.uppest=1 +CH1.Slope.sweep=0 +Math.mathon=0 +CH4.Video.module=0 +Bus1.LIN.source=0 +CH3.Video.sync=0 +CH2.rgb=FFFF00 +MeasureTimes=20 +Square.offset=0.0 +CH2.Video.holdoff=1 +SingleTrg.LIN.level=0 +Print.leftEdgeLength=20 +CH1.Pulse.voltsense=0 +CH1.vbIdx=9 +SingleTrg.LIN.data=0.0.0.0.0.0.0.0 +CH1.Slope.lowest=1 +CH3.trgMode=0 +productParam=VDS6104 +SingleTrg.CAN.coupling=0 +SingleTrg.CAN.stype=0 +CH3.pos0=0 +Sample.precision=0 +CH2.forcebandlimit=0 +CH2.couplingIdx=1 +CH1.Video.holdoff=1 +CH3.bandlimit=0 +CH1.Pulse.condition=0 +Print.downEdgeLength=20 +CH1.rgb=FF0000 +SingleTrg.CAN.dlc=0 +SingleTrg.Edge.sweep=0 +GridBrightness=100 +SingleTrg.Pulse.argument=3 +Sine.frequency=1000.0 +CH4.Pulse.holdoff=1 +NetWork.linkPort=2000 +MarkCursor.onTimebase=0 +MarkCursor.onVoltbase=0 +CH4.Edge.voltsense=0 +CH1.inverse=0 +Tune.chidx=0 +Bus2.LIN=LIN +SingleTrg.Edge.holdoff=1 +SingleTrg.sweep=0 +Rule.ruleOnPF=1 +MeasureChannels=0,1,2,3, +Variant= +SingleTrg.Slope.argument=3 +FFTCursor.y2=0 +FFTCursor.y1=0 +Bus2.LIN.samplePoint=50.0 +CH2.Edge.sweep=0 +CH3.Slope.lowest=1 +Zoom.offset=0 +SingleTrg.Edge.raisefall=0 +CH2.Pulse.argument=3 +CH2.Edge.holdoff=1 +productVersion=V2.0.2.011 +SingleTrg.Pulse.condition=1 +CH1.Video.module=0 +FFTCursor.x2=-6 +FFTCursor.x1=-6 +CH3.probeMultiIdx=0 +CH2.pos0=0 +Print.upEdgeLength=20 +CH4.Pulse.coupling=0 +\u00EF\u00BB\u00BF\#\u00E4\u00BB\u00A5\u00E4\u00B8\u008B\u00E4\u00B8\u00BA\u00E4\u00BA\u00A7\u00E5\u0093\u0081\u00E5\u008F\u00AF\u00E5\u008F\u0098\u00E5\u00AD\u0098\u00E5\u0082\u00A8\u00E9\u0085\u008D\u00E7\u00BD\u00AE\u00E4\u00BF\u00A1\u00E6\u0081\u00AF\u00E7\u009A\u0084\u00E9\u00BB\u0098\u00E8\u00AE\u00A4\u00E5\u0080\u00BC= +Math.fftchl=0 +Bus1.LIN=LIN +log_type=4 +getDataPeroid=40 +CH1.Edge.voltsense=0 +CH2.Slope.argument=3 +ReferenceSourceIdx=0 +Sample.avgTimes=1 diff --git a/docs/assets/img/measurements/Burst_20.png b/docs/assets/img/measurements/Burst_20.png new file mode 100644 index 0000000..96d1a84 Binary files /dev/null and b/docs/assets/img/measurements/Burst_20.png differ diff --git a/docs/assets/img/measurements/Burst_50.png b/docs/assets/img/measurements/Burst_50.png new file mode 100644 index 0000000..b092dbc Binary files /dev/null and b/docs/assets/img/measurements/Burst_50.png differ diff --git a/docs/assets/img/measurements/CEI 61000-3-2.png b/docs/assets/img/measurements/CEI 61000-3-2.png new file mode 100644 index 0000000..d871b5f Binary files /dev/null and b/docs/assets/img/measurements/CEI 61000-3-2.png differ diff --git a/docs/assets/img/measurements/Clyric_20.png b/docs/assets/img/measurements/Clyric_20.png new file mode 100644 index 0000000..a71ffcc Binary files /dev/null and b/docs/assets/img/measurements/Clyric_20.png differ diff --git a/docs/assets/img/measurements/Clyric_50.png b/docs/assets/img/measurements/Clyric_50.png new file mode 100644 index 0000000..bbd8c6d Binary files /dev/null and b/docs/assets/img/measurements/Clyric_50.png differ diff --git a/docs/assets/img/measurements/Dimmer 4_, 5_ ou 6_.jpeg b/docs/assets/img/measurements/Dimmer 4_, 5_ ou 6_.jpeg new file mode 100644 index 0000000..3cd70b7 Binary files /dev/null and b/docs/assets/img/measurements/Dimmer 4_, 5_ ou 6_.jpeg differ diff --git a/docs/assets/img/measurements/Dimmer 99_.jpeg b/docs/assets/img/measurements/Dimmer 99_.jpeg new file mode 100644 index 0000000..ab37cf5 Binary files /dev/null and b/docs/assets/img/measurements/Dimmer 99_.jpeg differ diff --git a/docs/assets/img/measurements/H15.png b/docs/assets/img/measurements/H15.png new file mode 100644 index 0000000..1f0edf3 Binary files /dev/null and b/docs/assets/img/measurements/H15.png differ diff --git a/docs/assets/img/measurements/H3.png b/docs/assets/img/measurements/H3.png new file mode 100644 index 0000000..162d4af Binary files /dev/null and b/docs/assets/img/measurements/H3.png differ diff --git a/docs/assets/img/measurements/Harmoniques.jpeg b/docs/assets/img/measurements/Harmoniques.jpeg new file mode 100644 index 0000000..1f17f01 Binary files /dev/null and b/docs/assets/img/measurements/Harmoniques.jpeg differ diff --git a/docs/assets/img/measurements/Oscillo_600W_Out.jpeg b/docs/assets/img/measurements/Oscillo_600W_Out.jpeg new file mode 100755 index 0000000..4c5403e Binary files /dev/null and b/docs/assets/img/measurements/Oscillo_600W_Out.jpeg differ diff --git a/docs/assets/img/measurements/Oscillo_Bypass.jpeg b/docs/assets/img/measurements/Oscillo_Bypass.jpeg new file mode 100644 index 0000000..066e964 Binary files /dev/null and b/docs/assets/img/measurements/Oscillo_Bypass.jpeg differ diff --git a/docs/assets/img/measurements/Oscillo_Dim_20_In.jpeg b/docs/assets/img/measurements/Oscillo_Dim_20_In.jpeg new file mode 100755 index 0000000..0b89ac1 Binary files /dev/null and b/docs/assets/img/measurements/Oscillo_Dim_20_In.jpeg differ diff --git a/docs/assets/img/measurements/Oscillo_Dim_20_Out.jpeg b/docs/assets/img/measurements/Oscillo_Dim_20_Out.jpeg new file mode 100755 index 0000000..8fad0af Binary files /dev/null and b/docs/assets/img/measurements/Oscillo_Dim_20_Out.jpeg differ diff --git a/docs/assets/img/measurements/Oscillo_Dimmer_20.jpeg b/docs/assets/img/measurements/Oscillo_Dimmer_20.jpeg new file mode 100644 index 0000000..f76709c Binary files /dev/null and b/docs/assets/img/measurements/Oscillo_Dimmer_20.jpeg differ diff --git a/docs/assets/img/measurements/Oscillo_Dimmer_50.jpeg b/docs/assets/img/measurements/Oscillo_Dimmer_50.jpeg new file mode 100644 index 0000000..0de0994 Binary files /dev/null and b/docs/assets/img/measurements/Oscillo_Dimmer_50.jpeg differ diff --git a/docs/assets/img/measurements/Oscillo_Dimmer_80.jpeg b/docs/assets/img/measurements/Oscillo_Dimmer_80.jpeg new file mode 100644 index 0000000..1e66282 Binary files /dev/null and b/docs/assets/img/measurements/Oscillo_Dimmer_80.jpeg differ diff --git a/docs/assets/img/measurements/Oscillo_ZCD.jpeg b/docs/assets/img/measurements/Oscillo_ZCD.jpeg new file mode 100644 index 0000000..67afbcb Binary files /dev/null and b/docs/assets/img/measurements/Oscillo_ZCD.jpeg differ diff --git a/docs/assets/img/measurements/Oscillo_ZCD_Robodyn.jpeg b/docs/assets/img/measurements/Oscillo_ZCD_Robodyn.jpeg new file mode 100644 index 0000000..23d94cd Binary files /dev/null and b/docs/assets/img/measurements/Oscillo_ZCD_Robodyn.jpeg differ diff --git a/docs/assets/img/measurements/Robodyn_duty_10.png b/docs/assets/img/measurements/Robodyn_duty_10.png new file mode 100644 index 0000000..a91448c Binary files /dev/null and b/docs/assets/img/measurements/Robodyn_duty_10.png differ diff --git a/docs/assets/img/measurements/Robodyn_duty_2047.png b/docs/assets/img/measurements/Robodyn_duty_2047.png new file mode 100644 index 0000000..3e4125a Binary files /dev/null and b/docs/assets/img/measurements/Robodyn_duty_2047.png differ diff --git a/docs/assets/img/measurements/Robodyn_duty_4090.png b/docs/assets/img/measurements/Robodyn_duty_4090.png new file mode 100644 index 0000000..c7cf8cf Binary files /dev/null and b/docs/assets/img/measurements/Robodyn_duty_4090.png differ diff --git a/docs/assets/img/measurements/VDS6104_10.png b/docs/assets/img/measurements/VDS6104_10.png new file mode 100644 index 0000000..3ad339d Binary files /dev/null and b/docs/assets/img/measurements/VDS6104_10.png differ diff --git a/docs/assets/img/measurements/VDS6104_50.png b/docs/assets/img/measurements/VDS6104_50.png new file mode 100644 index 0000000..c6608fe Binary files /dev/null and b/docs/assets/img/measurements/VDS6104_50.png differ diff --git a/docs/assets/img/measurements/VDS6104_90.png b/docs/assets/img/measurements/VDS6104_90.png new file mode 100644 index 0000000..e13efff Binary files /dev/null and b/docs/assets/img/measurements/VDS6104_90.png differ diff --git a/docs/assets/img/measurements/ZCD DanStar Falling.jpeg b/docs/assets/img/measurements/ZCD DanStar Falling.jpeg new file mode 100644 index 0000000..72f81a2 Binary files /dev/null and b/docs/assets/img/measurements/ZCD DanStar Falling.jpeg differ diff --git a/docs/assets/img/measurements/ZCD DanStar Rising.jpeg b/docs/assets/img/measurements/ZCD DanStar Rising.jpeg new file mode 100644 index 0000000..282a9a7 Binary files /dev/null and b/docs/assets/img/measurements/ZCD DanStar Rising.jpeg differ diff --git a/docs/assets/img/measurements/ZCD Robodyn Falling.jpeg b/docs/assets/img/measurements/ZCD Robodyn Falling.jpeg new file mode 100644 index 0000000..7522ee9 Binary files /dev/null and b/docs/assets/img/measurements/ZCD Robodyn Falling.jpeg differ diff --git a/docs/assets/img/measurements/ZCD Robodyn Rising.jpeg b/docs/assets/img/measurements/ZCD Robodyn Rising.jpeg new file mode 100644 index 0000000..ce1fd7a Binary files /dev/null and b/docs/assets/img/measurements/ZCD Robodyn Rising.jpeg differ diff --git a/docs/assets/img/measurements/ZCD_duty_10.png b/docs/assets/img/measurements/ZCD_duty_10.png new file mode 100644 index 0000000..ab91414 Binary files /dev/null and b/docs/assets/img/measurements/ZCD_duty_10.png differ diff --git a/docs/assets/img/measurements/ZCD_duty_2047.png b/docs/assets/img/measurements/ZCD_duty_2047.png new file mode 100644 index 0000000..47884e0 Binary files /dev/null and b/docs/assets/img/measurements/ZCD_duty_2047.png differ diff --git a/docs/assets/img/measurements/ZCD_duty_4090.png b/docs/assets/img/measurements/ZCD_duty_4090.png new file mode 100644 index 0000000..2d1418d Binary files /dev/null and b/docs/assets/img/measurements/ZCD_duty_4090.png differ diff --git a/docs/assets/img/schemas/Breaker_16A.jpeg b/docs/assets/img/schemas/Breaker_16A.jpeg new file mode 100644 index 0000000..a1e4dcf Binary files /dev/null and b/docs/assets/img/schemas/Breaker_16A.jpeg differ diff --git a/docs/assets/img/schemas/Breaker_2A.jpeg b/docs/assets/img/schemas/Breaker_2A.jpeg new file mode 100644 index 0000000..da92537 Binary files /dev/null and b/docs/assets/img/schemas/Breaker_2A.jpeg differ diff --git a/docs/assets/img/schemas/Contact_25A.jpeg b/docs/assets/img/schemas/Contact_25A.jpeg new file mode 100644 index 0000000..0799fda Binary files /dev/null and b/docs/assets/img/schemas/Contact_25A.jpeg differ diff --git a/docs/assets/img/schemas/LSA.png b/docs/assets/img/schemas/LSA.png new file mode 100644 index 0000000..ec81e0d Binary files /dev/null and b/docs/assets/img/schemas/LSA.png differ diff --git a/docs/assets/img/schemas/RC_Snubber.jpeg b/docs/assets/img/schemas/RC_Snubber.jpeg new file mode 100644 index 0000000..b97d359 Binary files /dev/null and b/docs/assets/img/schemas/RC_Snubber.jpeg differ diff --git a/docs/assets/img/schemas/Relay.png b/docs/assets/img/schemas/Relay.png new file mode 100644 index 0000000..359233a Binary files /dev/null and b/docs/assets/img/schemas/Relay.png differ diff --git a/docs/assets/img/schemas/Shelly_Dimmer_0_1-10V_PM_Gen3.png b/docs/assets/img/schemas/Shelly_Dimmer_0_1-10V_PM_Gen3.png new file mode 100644 index 0000000..2c827f1 Binary files /dev/null and b/docs/assets/img/schemas/Shelly_Dimmer_0_1-10V_PM_Gen3.png differ diff --git a/docs/assets/img/schemas/Shelly_EM_50.png b/docs/assets/img/schemas/Shelly_EM_50.png new file mode 100644 index 0000000..24bcf7a Binary files /dev/null and b/docs/assets/img/schemas/Shelly_EM_50.png differ diff --git a/docs/assets/img/schemas/Solar_Router_Diverter.jpg b/docs/assets/img/schemas/Solar_Router_Diverter.jpg new file mode 100644 index 0000000..497e67c Binary files /dev/null and b/docs/assets/img/schemas/Solar_Router_Diverter.jpg differ diff --git a/docs/assets/img/schemas/water_tank.jpeg b/docs/assets/img/schemas/water_tank.jpeg new file mode 100644 index 0000000..bbf7525 Binary files /dev/null and b/docs/assets/img/schemas/water_tank.jpeg differ diff --git a/docs/assets/img/screenshots/Captive_Portal.jpeg b/docs/assets/img/screenshots/Captive_Portal.jpeg new file mode 100644 index 0000000..5691128 Binary files /dev/null and b/docs/assets/img/screenshots/Captive_Portal.jpeg differ diff --git a/docs/assets/img/screenshots/Espressif_Flash_Tool.png b/docs/assets/img/screenshots/Espressif_Flash_Tool.png new file mode 100644 index 0000000..e2f434f Binary files /dev/null and b/docs/assets/img/screenshots/Espressif_Flash_Tool.png differ diff --git a/docs/assets/img/screenshots/config.jpeg b/docs/assets/img/screenshots/config.jpeg new file mode 100644 index 0000000..296e968 Binary files /dev/null and b/docs/assets/img/screenshots/config.jpeg differ diff --git a/docs/assets/img/screenshots/console.jpeg b/docs/assets/img/screenshots/console.jpeg new file mode 100644 index 0000000..16074f3 Binary files /dev/null and b/docs/assets/img/screenshots/console.jpeg differ diff --git a/docs/assets/img/screenshots/display.gif b/docs/assets/img/screenshots/display.gif new file mode 100644 index 0000000..45b5d1a Binary files /dev/null and b/docs/assets/img/screenshots/display.gif differ diff --git a/docs/assets/img/screenshots/display_example.jpeg b/docs/assets/img/screenshots/display_example.jpeg new file mode 100644 index 0000000..0ecc270 Binary files /dev/null and b/docs/assets/img/screenshots/display_example.jpeg differ diff --git a/docs/assets/img/screenshots/gpio.jpeg b/docs/assets/img/screenshots/gpio.jpeg new file mode 100644 index 0000000..d456672 Binary files /dev/null and b/docs/assets/img/screenshots/gpio.jpeg differ diff --git a/docs/assets/img/screenshots/ha_disco_1.jpeg b/docs/assets/img/screenshots/ha_disco_1.jpeg new file mode 100644 index 0000000..a000118 Binary files /dev/null and b/docs/assets/img/screenshots/ha_disco_1.jpeg differ diff --git a/docs/assets/img/screenshots/ha_disco_2.jpeg b/docs/assets/img/screenshots/ha_disco_2.jpeg new file mode 100644 index 0000000..037b2cd Binary files /dev/null and b/docs/assets/img/screenshots/ha_disco_2.jpeg differ diff --git a/docs/assets/img/screenshots/hardware.jpeg b/docs/assets/img/screenshots/hardware.jpeg new file mode 100644 index 0000000..c95ea5f Binary files /dev/null and b/docs/assets/img/screenshots/hardware.jpeg differ diff --git a/docs/assets/img/screenshots/hardware_config.jpeg b/docs/assets/img/screenshots/hardware_config.jpeg new file mode 100644 index 0000000..a6a3bf4 Binary files /dev/null and b/docs/assets/img/screenshots/hardware_config.jpeg differ diff --git a/docs/assets/img/screenshots/management.jpeg b/docs/assets/img/screenshots/management.jpeg new file mode 100644 index 0000000..123f20e Binary files /dev/null and b/docs/assets/img/screenshots/management.jpeg differ diff --git a/docs/assets/img/screenshots/mqtt.jpeg b/docs/assets/img/screenshots/mqtt.jpeg new file mode 100644 index 0000000..f2a03f5 Binary files /dev/null and b/docs/assets/img/screenshots/mqtt.jpeg differ diff --git a/docs/assets/img/screenshots/mqtt_explorer.jpeg b/docs/assets/img/screenshots/mqtt_explorer.jpeg new file mode 100644 index 0000000..550b5e0 Binary files /dev/null and b/docs/assets/img/screenshots/mqtt_explorer.jpeg differ diff --git a/docs/assets/img/screenshots/network.jpeg b/docs/assets/img/screenshots/network.jpeg new file mode 100644 index 0000000..307dd13 Binary files /dev/null and b/docs/assets/img/screenshots/network.jpeg differ diff --git a/docs/assets/img/screenshots/output1.jpeg b/docs/assets/img/screenshots/output1.jpeg new file mode 100644 index 0000000..19613d2 Binary files /dev/null and b/docs/assets/img/screenshots/output1.jpeg differ diff --git a/docs/assets/img/screenshots/output2.jpeg b/docs/assets/img/screenshots/output2.jpeg new file mode 100644 index 0000000..cbef33c Binary files /dev/null and b/docs/assets/img/screenshots/output2.jpeg differ diff --git a/docs/assets/img/screenshots/overview.jpeg b/docs/assets/img/screenshots/overview.jpeg new file mode 100644 index 0000000..0029c6f Binary files /dev/null and b/docs/assets/img/screenshots/overview.jpeg differ diff --git a/docs/assets/img/screenshots/pid_tuning.jpeg b/docs/assets/img/screenshots/pid_tuning.jpeg new file mode 100644 index 0000000..6369aba Binary files /dev/null and b/docs/assets/img/screenshots/pid_tuning.jpeg differ diff --git a/docs/assets/img/screenshots/relays.jpeg b/docs/assets/img/screenshots/relays.jpeg new file mode 100644 index 0000000..3ebe567 Binary files /dev/null and b/docs/assets/img/screenshots/relays.jpeg differ diff --git a/docs/assets/img/screenshots/remote-jsy-1.jpeg b/docs/assets/img/screenshots/remote-jsy-1.jpeg new file mode 100644 index 0000000..1224c7b Binary files /dev/null and b/docs/assets/img/screenshots/remote-jsy-1.jpeg differ diff --git a/docs/assets/img/screenshots/remote-jsy-2.jpeg b/docs/assets/img/screenshots/remote-jsy-2.jpeg new file mode 100644 index 0000000..d8fb46b Binary files /dev/null and b/docs/assets/img/screenshots/remote-jsy-2.jpeg differ diff --git a/docs/assets/img/screenshots/shelly_script_id.jpeg b/docs/assets/img/screenshots/shelly_script_id.jpeg new file mode 100644 index 0000000..dcb33aa Binary files /dev/null and b/docs/assets/img/screenshots/shelly_script_id.jpeg differ diff --git a/docs/assets/img/screenshots/statistics.jpeg b/docs/assets/img/screenshots/statistics.jpeg new file mode 100644 index 0000000..604da48 Binary files /dev/null and b/docs/assets/img/screenshots/statistics.jpeg differ diff --git a/docs/assets/img/screenshots/update.jpeg b/docs/assets/img/screenshots/update.jpeg new file mode 100644 index 0000000..684adb2 Binary files /dev/null and b/docs/assets/img/screenshots/update.jpeg differ diff --git a/docs/blog.md b/docs/blog.md new file mode 100644 index 0000000..5b54f4f --- /dev/null +++ b/docs/blog.md @@ -0,0 +1,12 @@ +--- +layout: default +title: Blog +description: Blog +--- + +# Blog + +- [2024-07-01 - Shelly Solar Diverter](/blog/2024-07-01_shelly_solar_diverter) +- [2024-06-26 - Everything on le JSY](/blog/2024-06-26_everything_on_the_jsy) +- [2024-06-25 - Remote JSY through UDP](/blog/2024-06-25_remote_jsy) +- [2024-06-23 - Development is still in progress](/blog/2024-06-23_development_is_still_in_progress) diff --git a/docs/blog/2024-06-23_development_is_still_in_progress.md b/docs/blog/2024-06-23_development_is_still_in_progress.md new file mode 100644 index 0000000..8984077 --- /dev/null +++ b/docs/blog/2024-06-23_development_is_still_in_progress.md @@ -0,0 +1,17 @@ +--- +layout: default +title: Blog +description: Development is still in progress at a good pace +--- + +_Date: 2024-06-23_ + +# Development is still in progress + +Development of YaSolR is still in progress at a good pace. +You can follow the progress in the [GitHub project](https://github.com/mathieucarbou/YaSolR-OSS/projects?query=is%3Aopen) view. + +- The analysis is completed on real data to determine a good routing algorithm using a PID controller with a tweaked Kp, Ki and Kd in order to minimize the grid import and export as much as possible. +- This PID algorithm will recompute each time a power change is detected, which means in a few milliseconds with a JSY. +- The routing precision is high with a 12-bits resolution +- The remote JSY feature through UDP is being implemented to facilitate testing diff --git a/docs/blog/2024-06-25_remote_jsy.md b/docs/blog/2024-06-25_remote_jsy.md new file mode 100644 index 0000000..068d247 --- /dev/null +++ b/docs/blog/2024-06-25_remote_jsy.md @@ -0,0 +1,41 @@ +--- +layout: default +title: Blog +description: Remote JSY through UDP +--- + +_Date: 2024-06-25_ + +# Remote JSY + +The free [JSY library](https://oss.carbou.me/MycilaJSY/) has been completed with 2 new examples to show how to use the JSY remotely through UDP. + +The `Sender` program must be uploaded to an ESP32 connected to a JSY. +It sends through UDP broadcast the JSY data several times per second. +It can also be used as a standalone app to display the JSY data in real-time. + +![](https://github.com/mathieucarbou/MycilaJSY/assets/61346/3066bf12-31d5-45de-9303-d810f14731d0) + +There is also a `Listener` example which is the same app, bit not connected to the JSY, but will receive the data through UDP and display the metrics. + +The 2 samples are made with ESP-DASH, ElegantOTA, WebSerial, MycilaESpConnect, etc. + +=> [MycilaJSY RemoteUDP](https://github.com/mathieucarbou/MycilaJSY/tree/main/examples/RemoteUDP) +=> [MycilaJSY](https://oss.carbou.me/MycilaJSY/) project + +## Remote JSY in YaSolR + +The YaSolR project also support the JSY `Sender`. YaSolR will listen for these UDP packets and will use the remote JSY data if they are available. + +You can have an ESP32 board installed in your electric box with a JSY and the `Sender` app, and YaSolr firmware installed next to the loads. + +When using a remote JSY in YaSolR, the following rules apply: + +- The voltage will always be read if possible from a connected JSY or PZEM, then from a remote JSY, then from MQTT. +- The grid power will always be read first from MQTT, then from a remote JSY, then from a connected JSY. + +## Speed + +The JSY Remote through UDP is nearly as fast as having the JSY wired to the ESP. +All changes to the JSY are immediately sent through UDP to the listener at a rate of about **20 messages per second**. +This is the rate at which the JSY usually updates its data. diff --git a/docs/blog/2024-06-26_everything_on_the_jsy.md b/docs/blog/2024-06-26_everything_on_the_jsy.md new file mode 100644 index 0000000..4baf3b7 --- /dev/null +++ b/docs/blog/2024-06-26_everything_on_the_jsy.md @@ -0,0 +1,104 @@ +--- +layout: default +title: Blog +description: Everything on the JSY +--- + +_Date: 2024-06-26_ + +# Everything on the JSY + +I am developing quite a few projects and librairies in the Arduino / ESP32 / Home automation landscape ([https://oss.carbou.me](https://oss.carbou.me)), including YaSolR routing software as well. + +Last year I created a specialized library for the JSY, which I wanted to talk to you about today in order to present its operation and use in the context of a solar router. + +## How the JSY is used in a solar router + +A solar router needs to measure the current, to react to these measurements and propose a new duty cycle to apply to the dimmer, for a certain time, until it obtains a new measurement. +So it depends on the speed at which these measurements are taken, and their precision, but also of course on the routing algorithm used and the precision of the steps (0-100, 8-bit, 12-bit, etc.). + +It's rare to find a solar router that correctly uses JSY at its full capacity, partly due to the severe lack of an effective library for JSY, and that's why I created it. + +## Asynchronous operation + +As JSY slows down routing, it is important to be able to retrieve its values ​​as quickly as possible. +Most libraries use delays or read() in loops, which is non-blocking, good, but does not guarantee reading the data as soon as it is ready because these read tests depend on the speed of execution of the loop. + +Not many people know this, but Arduino offers a serial read method (Serial.readBytes()), which is implemented differently from the non-blocking read(): readBytes() is blocking and directly uses the UART interrupts backwards to unlock as soon as the data is ready. +This is the most effective method to be notified immediately of the availability of measurements. +It is then enough to set up a reading loop in an asynchronous task on core 0 of the ESP32, to be notified as soon as the data is ready. + +## JSY reading speeds + +The JSY speed can be changed from its default (4800 bps) to 9600, 19200 and 38400 bps. + +- At 4800 bps, the JSY takes on average 171 ms to make a reading +- At 9600 bps, the JSY takes on average 100 ms to make a reading +- At 19200 bps, the JSY takes on average 60 ms to take a reading +- At 38400 bps, the JSY takes on average 41 ms to take a reading + +So increasing the speed of your JSY from 4800 to 38400 potentially allows you to react 4x faster to these readings... +But are these readings meaningful? +Let's see... + +## Internal workings of JSY + +The JSY works with an Renergy RN8209G chip, which continuously measures by taking a rolling average and makes the results available on the UART. +For example. if you read the JSY repeatedly: + +- At 4800 bps, the JSY reports on average one change every 3 readings, or 513 ms +- At 9600 bps, the JSY reports on average a change every 4-5 readings, or 400-500 ms +- At 19200 bps, the JSY reports on average one change every 9 readings, or 360 ms +- At 38400 bps, the JSY reports on average one change every 9 readings, or 369 ms + +So it is not possible to have a router that will make a correction to the routing faster than this minimum delay for the JSY to detect a change in the measurements. +So the routing algorithm should apply for at least 300 ms. + +## Load Detection Time + +The JSY has a load detection time. +For example, when turning on a load of 0-100%, it takes a certain amount of time to start making its data available. + +- At 4800 bps, the JSY takes approximately 680 ms to detect a load turning on +- At 9600 bps, the JSY takes approximately 400-700 ms to detect a load turning on +- At 19200 bps, the JSY takes approximately 360 ms to detect a load turning on +- At 38400 bps, the JSY takes approximately 330 ms to detect a load turning on + +This is the minimum time that the JSY takes to make a measurement available after a load change. + +## Ramp-up Time + +When a load is on from 0 to 100%, goes from 0 to 3000W for example, the JSY takes time to see the nominal power because it uses moving average. + +- At 4800 bps, the JSY has a ramp-up time (time before seeing nominal power) of 1198 ms +- At 9600 bps, JSY has a ramp-up time of 800-1101 ms +- At 19200 bps, JSY has a ramp-up time of 1020 ms +- At 38400 bps, the JSY has a ramp-up time of 1030 ms + +This is pretty consistent, and it's the JSY window duration, which is about 1 second. + +## Using JSY in remote mode + +The library includes a `Sender` application and another `Listener`. +The sender application is a standalone application to flash on an ESP32 connected to a JSY. +It uses ESP-DASH, ElegantOTA, MycilaESPConnect, etc. and allows you to see all the JSY stats, reset the energy, and, above all, sends the measurements via UDP at a **speed of 20 messages per second**, which is as fast as than reading the data locally at 38400 bps on an JSY connected to the ESP32. +The `Listener` application shows how to receive them at a processing speed of 20 messages per second. + +![](https://github.com/mathieucarbou/MycilaJSY/assets/61346/3066bf12-31d5-45de-9303-d810f14731d0) + +## Conclusion + +- Increasing the speed of the JSY does not serve to read more measurements, but above all serves to be notified as quickly as possible when new measurements arrive, without having to wait for the loop to return. +- The JSY appears to have a sliding window of 1 second, and appears to make its measurements available every 300 ms +- Whether the JSY is connected to the ESP or whether the data is retrieved remotely by UDP with the `Sender` application does not change the speed or precision of routing + +## MycilaJSY Library + +The library is here, with performance tests based on speeds: [https://github.com/mathieucarbou/MycilaJSY](https://github.com/mathieucarbou/MycilaJSY) + +It supports: +- Non-blocking mode with asynchronous task +- Callbacks to be notified of metric readings and changes +- Energy reset +- Gear switch +- Remote operation via UDP at a speed of 20 messages per second diff --git a/docs/blog/2024-07-01_shelly_solar_diverter.md b/docs/blog/2024-07-01_shelly_solar_diverter.md new file mode 100644 index 0000000..49cdf74 --- /dev/null +++ b/docs/blog/2024-07-01_shelly_solar_diverter.md @@ -0,0 +1,291 @@ +--- +layout: default +title: Blog +description: "Shelly Solar Diverter / Router: redirects the excess solar production to a water tank or heater" +--- + +_Date: 2024-07-01_ + +_I've put the YaSolR project in pause for a few days to work on this very cool and awesome Shelly integration..._ + +# Shelly Solar Diverter + +- [What is a Solar Router / Diverter ?](#what-is-a-solar-router--diverter-) +- [Shelly Solar Diverter](#shelly-solar-diverter-1) +- [Download](#download) +- [Hardware](#hardware) +- [Wiring](#wiring) + - [Shelly Add-On + DS18B20](#shelly-add-on--ds18b20) + - [Electric Circuit](#electric-circuit) + - [RC Snubber](#rc-snubber) + - [Add other dimmers](#add-other-dimmers) +- [Setup](#setup) + - [Shelly Dimmer Setup](#shelly-dimmer-setup) + - [Shelly Pro EM 50 Setup](#shelly-pro-em-50-setup) +- [How to use](#how-to-use) + - [Configuration](#configuration) + - [Several dimmers](#several-dimmers) + - [Excess sharing amongst dimmers](#excess-sharing-amongst-dimmers) + - [Start / Stop Automatic Divert](#start--stop-automatic-divert) + - [Solar Diverter Status](#solar-diverter-status) +- [Demo](#demo) + +## What is a Solar Router / Diverter ? + +A _Solar Router_ allows to redirect the solar production excess to some appliances instead of returning it to the grid. +The particularity of a solar router is that it will dim the voltage and power sent to the appliance in order to match the excess production, in contrary to a simple relay that would just switch on/off the appliance without controlling its power. + +A _Solar Router_ is usually connected to the resistance of a water tank and will heat the water when there is production excess. + +A solar router can also do more things, like controlling (on/off) the activation of other appliances (with the grid normal voltage and not the dimmed voltage) in case the excess reaches a threshold. For example, one could activate a pump, pool heater, etc if the excess goes above a specific amount, so that this appliance gets the priority over heating the water tank. + +A router can also schedule some forced heating of the water tank to ensure the water reaches a safe temperature, and consequently bypass the dimmed voltage. This is called a bypass relay. + +## Shelly Solar Diverter + +- **Unlimited dimmers (output)** +- **PID Controller** +- **Excess sharing amongst dimmers with percentages** +- **Plus all the power of the Shelly ecosystem (rules, schedules, automations, etc)** + +This solar diverter based on Shelly devices and a Shelly script can control remotely dimmers and could even be enhanced with relays. +Shelly's being remotely controllable, such system offers a very good integration with Shelly App and Home Automation Systems like Home Assistant. + +It is possible to put some rules based on temperature, time, days, etc and control everything from the Shelly App or Home Assistant. + +The Shelly script, when activated, automatically adjusts the dimmers to the grid import or export (solar production excess). + +## Download + +- **[Shelly Solar Diverter Script](/downloads/solar_diverter_v1.js)** + +## Hardware + +All the components can be bought at [https://www.shelly.com/](https://www.shelly.com/), except the voltage regulator, where you can find some links [on my website](/build#compatible-hardware) + +| [Shelly Pro EM - 50](https://www.shelly.com/fr/products/shop/proem-1x50a) | [Shelly Dimmer 0/1-10V PM Gen3](https://www.shelly.com/fr/products/shop/1xsd10pmgen3) | [Shelly Plus Add-On](https://www.shelly.com/fr/products/shop/shelly-plus-add-on) | [Temperature Sensor DS18B20](https://www.shelly.com/fr/products/shop/temperature-sensor-ds18B20) | Voltage Regulator
- [Loncont LSA-H3P50YB](https://fr.aliexpress.com/item/32606780994.html)
- [LCTC DTY-220V40P1](https://fr.aliexpress.com/item/1005005008018888.html) | +| :-----------------------------------------------------------------------: | :-----------------------------------------------------------------------------------: | :------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +| ![](/assets/img/hardware/Shelly_Pro_EM_50.jpeg) | ![](/assets/img/hardware/Shelly_Dimmer-10V.jpeg) | ![](/assets/img/hardware/Shelly_Addon.jpeg) | ![](/assets/img/hardware/Shelly_DS18.jpeg) | ![](/assets/img/hardware/LSA-H3P50YB.jpeg)
![](/assets/img/hardware/LCTC_Voltage_Regulator_DTY-220V40P1.jpeg) | + +Some additional hardware are required depending on the installation. +**Please select the amperage according to your needs.** + +- A 2A breaker for the Shelly electric circuit +- A 16A or 20A breaker for your water tank (resistance) electric circuit +- A 25A relay or contactor for the bypass relay (to force a heating) for the water tank electric circuit +- A protection box for the Shelly +- **The LSA voltage regulator is far better than the LCTC one**: the variation of voltage from 0 to 10V better matches the power curve of the output + +## Wiring + +### Shelly Add-On + DS18B20 + +First the easy part: the temperature sensor and the Shelly Add-On, which has to be put behind the Shelly Dimmer. + +![](/assets/img/hardware/Shelly_Addon_DS18.jpeg) + +### Electric Circuit + +- Choose your breakers and wires according to your load +- Circuits can be split. + For example, the Shelly EM can be inside the main electric box, and the Shelly Dimmer + Add-On can be in the water tank electric panel, while the contactor and dimmer can be placed neat the water tank. + They communicate through the network. +- The dimmer will control the voltage regulator through the `COM` and `0-10V` ports +- The dimmer will also control the relay or contactor through the `A2` ports +- The wire from `Dimmer Output` to `Dimmer S1` is to set the switch mode to invert and make the dimmer detect when the contactor is OFF or ON and respectively disable or enable the dimming. +- The B clamp around the wire going from the voltage regulator to the water tank is to measure the current going through the water tank resistance is optional and for information purposes only. +- The A clamp should be put around the main phase entering the house +- The relay / contactor is optional and is used to schedule some forced heating of the water tank to ensure the water reaches a safe temperature, and consequently bypass the dimmed voltage. +- The neutral wire going to the voltage regulator can be a small one; it is only used for the voltage and Zero-Crossing detection. +- Communication is done through WiFI: **make sure you have a good WiFi to reduce the connection time and improve the router speed.** + +[![](/assets/img/schemas/Solar_Router_Diverter.jpg)](/assets/img/schemas/Solar_Router_Diverter.jpg) + +### RC Snubber + +If switching the contactor / relay causes the Shelly device to reboot, place a [RC Snubber](https://www.shelly.com/fr/products/shop/shelly-rc-snubber) between the A1 and A2 ports of the contactor / relay. + +### Add other dimmers + +If you want to control a second resistive load, it is possible to duplicate the circuit to add another dimmer and voltage regulator. + +Modify the config accordingly to support many dimmers and they will be turned on/off sequentially to match the excess production. + +## Setup + +First make sure that your Shelly's are setup properly. + +The script has to be installed inside the Shelly Pro EM 50, because this is where the measurements of the imported and exported grid power is done. +Also, this central place allows to control the 1, 2 or more dimmers remotely. + +### Shelly Dimmer Setup + +- Make sure the switch (input) are enabled and inverted. S1 should replicate the inverse state of the relay / contactor, so that when you activate the contactor through the Dimmer output remotely, the dimmer will deactivate itself. +- Set static IP addresses + +### Shelly Pro EM 50 Setup + +- Set static IP address +- Make sure to place the A clamp around the main phase entering the house in the right direction +- Add the `Shelly Solar Diverter` to the Shelly Pro EM +- Configure the settings in the `CONFIG` object +- Start teh script + +## How to use + +### Configuration + +Edit the `CONFIG` object and pay attention to the values, especially the resistance value which should be accurate, otherwise the routing precision will be bad. + +```javascript +const CONFIG = { + // Debug mode + DEBUG: 1, + // Grid Power Read Interval (s) + READ_INTERVAL_S: 1, + PID: { + // Target Grid Power (W) + SET_POINT: 0, + // Number of Watts allowed to be above or below the set point (W) + SET_POINT_DELTA: 2, + // PID Proportional Gain + KP: 0.8, + // PID Integral Gain + KI: 0, + // PID Derivative Gain + KD: 0.8, + // PID Output Minimum Clamp (W) + OUTPUT_MIN: 0, + }, + DIMMERS: { + "192.168.125.93": { + // Resistance (in Ohm) of the load connecter to the dimmer + voltage regulator + // 0 will disable the dimmer + RESISTANCE: 24, + // Percentage of the remaining excess power that will be assigned to this dimmer + // The remaining percentage will be given to the next dimmers + RESERVED_EXCESS_PERCENT: 100, + }, + "192.168.125.97": { + RESISTANCE: 0, + RESERVED_EXCESS_PERCENT: 100, + }, + }, +}; +``` + +### Several dimmers + +The script can automatically control several dimmers. +Just add the IP address of the dimmer and the resistance of the load connected to it. + +**How it works:** + +If you have 2000W of excess, and 2 dimmers of 1500W each (nominal load), then the first ome will be set at 100% and will consume 1500W and the second one will consume the remaining 500W. + +### Excess sharing amongst dimmers + +It is possible to share the excess power amongst the dimmers. +Let's say you have 3 dimmers with this configuration: + +```javascript + DIMMERS: { + "192.168.125.93": { + RESISTANCE: 53, + RESERVED_EXCESS_PERCENT: 50, + }, + "192.168.125.94": { + RESISTANCE: 53, + RESERVED_EXCESS_PERCENT: 25, + }, + "192.168.125.95": { + RESISTANCE: 53, + RESERVED_EXCESS_PERCENT: 100, + }, +``` + +When you'll have 3000W of excess: + +- the first one will take up to 50% of it (1500 W), but it will only take 1000 W because of the resistance. So 2000 W remains. +- the second one 25% of teh remaining (500 W) +- the third one will take the remaining 1000 W + +### Start / Stop Automatic Divert + +You can start / stop the script manually from the interface or remotely by calling: + +``` + +http://192.168.125.92/rpc/Script.Start?id=1 +http://192.168.125.92/rpc/Script.Stop?id=1 + +``` + +- `192.168.125.92` begin the Shelly EM 50 static IP address. +- `1` being the script ID as seen in the Shelly interface + +![](/assets/img/screenshots/shelly_script_id.jpeg) + +Once the script is uploaded and started, it will automatically manage the power sent to the resistive load according to the rules above. + +### Solar Diverter Status + +You can view the status of the script by going to the script `status` endpoint, which is only available when the script is running. + +``` + +http://192.168.125.92/script/1/status + +``` + +```json +{ + "pid": { + "input": 0, + "output": 0, + "error": 0, + "pTerm": 0, + "iTerm": 0, + "dTerm": 0 + }, + "divert": { + "voltage": 237.9, + "gridPower": 0, + "divertPower": 173.76, + "dimmers": { + "192.168.125.93": { + "divertPower": 86.88, + "nominalPower": 2358.18375, + "dutyCycle": 0.03684191276, + "powerFactor": 0.19194247253, + "dimmedVoltage": 45.66311421705, + "current": 1.90262975904, + "apparentPower": 452.63561967657, + "thdi": 5.11302248812, + "rpc": "pending" + }, + "192.168.125.97": { + "divertPower": 86.88, + "nominalPower": 2358.18375, + "dutyCycle": 0.03684191276, + "powerFactor": 0.19194247253, + "dimmedVoltage": 45.66311421705, + "current": 1.90262975904, + "apparentPower": 452.63561967657, + "thdi": 5.11302248812, + "rpc": "pending" + } + } + } +} +``` + +## Demo + +Here is the view of the Shelly device websites while the grid power is changing. + +[![Shelly Solar Diverter Demo](http://img.youtube.com/vi/he5qPJx8_R4/0.jpg)](http://www.youtube.com/watch?v=he5qPJx8_R4 "Shelly Solar Diverter Demo") + +Here is a PoC box I am using for my testing with all the components wired. I am still waiting for the dimmer gen 3 which works in **current sourcing** mode, but everything else is working. + +![](/assets/img/hardware/shelly_solar_diverter_poc.jpeg) diff --git a/docs/build.md b/docs/build.md new file mode 100644 index 0000000..1180dc6 --- /dev/null +++ b/docs/build.md @@ -0,0 +1,344 @@ +--- +layout: default +title: Build +description: Build +--- + +# How to build your router + +- [Build Types](#build-types) + - [Dimmers: Solid State Relay or Robodyn ?](#dimmers-solid-state-relay-or-robodyn-) + - [Relays: Solid State Relay or Electromagnetic Relay ?](#relays-solid-state-relay-or-electromagnetic-relay-) +- [Compatible ESP32 Boards](#compatible-esp32-boards) +- [Compatible Hardware](#compatible-hardware) + - [How to choose your SSR ?](#how-to-choose-your-ssr-) + - [Examples of Shopping Lists](#examples-of-shopping-lists) +- [Default GPIO pinout per board](#default-gpio-pinout-per-board) +- [Pictures of some routers](#pictures-of-some-routers) + +## Build Types + +YaS☀️lR supports many builds and routing algorithms. +Before building your router, you need to decide which type of technology you want to use to dim the voltage (_Burst Mode_ or _Phase Control_). +Here is a compatibility matrix for the main pieces of hardware depending on the router type you want to build. + +Once you have picked up your build type, you can look at the Wiring Schema to know how to wire it and see the [Compatible Hardware](#compatible-hardware) section to know what you need to buy. + +| Hardware | Phase Control _(\*3)_ | Burst Mode | Nominal Load _(\*1)_ | Wiring Schemas | +| :------------------------------ | :-------------------: | :--------: | :---------------------------: | :------------------------- | +| Robodyn 24A
_(alone)_ | ✅ | ✅ | < 2000 W | // TODO: add wiring schema | +| Robodyn 40A
_(alone)_ | ✅ | ✅ | < 3000 W | // TODO: add wiring schema | +| Random SSR
+ ZCD Circuit | ✅ | ✅ | 1/3 of SSR rated load _(\*2)_ | // TODO: add wiring schema | +| Zero-Cross SSR
+ ZCD Circuit | ❌ | ✅ | 1/3 of SSR rated load _(\*2)_ | // TODO: add wiring schema | + +- **(\*1)**: In example, a 24A Triac would support a maximum peak of 24A but it is advised to not go over 1/3 - 1/2 for the nominal load. Some people also replace the TRIAC with a better one (see [Compatible Hardware](#compatible-hardware) below)\_ +- **(\*2)**: _Max Load for SSR depends on the supported SSR load. Use 1/3 rule for safety_ +- **(\*3)**: Generate _harmonics_, an effect of phase control system. This is not wrong if properly maintained at an acceptable level as per the regulations.\_ + +**Hint:** Remember that according to regulations about harmonic, the maximum power should be at around 750W. +More than that, you are supposed to put in place filtering systems to reduce the harmonics. +So if you want to build a router that is compliant with the regulations, any build type will be fine as long as you pay attention to the generated harmonic level. + +**Bypass:** Whatever the solution you pick, you can always add an optional bypass relay to send the full power to the load instead of dimming it, free of harmonics. +If no bypass relay is added, the dimmer will be used instead at 100% when pressing the bypass. + +### Dimmers: Solid State Relay or Robodyn ? + +Here are some pros and cons of each phase control system: + +**Robodyn (TRIAC):** + +- Pros: + - cheap and easy to wire + - 40A model comes with a heat sink and fan + - All in one device: phase control, ZCD, heat sink, fan +- Cons: + - limited in load to 1/3 - 1/2 of the announced load + - 16A / 24A models comes with heat sink which is too small for its supported maximum load + - no solution ready to attach them on a DIN rail. + - The heat sink often has to be upgraded, except for the one on the 40 model which is already good for small loads below 2000W. + - The ZCD circuit [is less accurate](https://github.com/fabianoriccardi/dimmable-light/wiki/About-dimmer-boards) and pulses can be harder to detect [on some boards](https://github.com/fabianoriccardi/dimmable-light/wiki/Notes-about-specific-architectures#interrupt-issue) + - You need to go over some modifications to ([improve wiring / soldering and heat sink](https://sites.google.com/view/le-professolaire/routeur-professolaire)) + - You might need to replace the Triac or move it + +**Solid State Relays:** + +- Pros: + - cheap and easy to wire + - support higher loads + - can be attached to a DIN rail with standard SSR clips + - lot of heat sink models available +- Cons: + - limited in load to 1/3 - 1/2 of the announced load + - require an external ZCD module, heat sink and/or fan + +**Heat Sink:** + +In any case, do not forget to correctly dissipate the heat of your Triac / SSR using an appropriate heat sink. +Robodyn heat sink is not enough and require some tweaking (like adding a flan or de-soldering the triac and heat sink and put the triac on a bigger heat sink). + +It is best to take a vertical heat sink for heat dissipation. +In case of the Robodyn 40A, you can install it vertically. + +### Relays: Solid State Relay or Electromagnetic Relay ? + +**Router output bypass relays:** + +- For bypass relays, you can use electromagnetic relays because they will be used less frequently. + Also, some electromagnetic relays boards have both a NO and NC output to better isolate the dimming circuit and bypass circuit. + +**Normal relays:** + +- If you want to use the relays to automatically switch one of the resistance of the water tank, as described in the [recommendations to reduce harmonics and flickering](./overview#recommendations-to-reduce-harmonics-and-flickering), you must use a SSR because the relay will be switched on and off frequently. + +- If you are not using the automatic relay switching, and you either control them manually or through a Home Automation system, you can use electromagnetic relays, providing the relays won't be switched on and off frequently. + +**Also to consider:** + +- You should not use electromagnetic relays to switch a load on and off frequently because they have a limited number of cycles before they fail and they can be stuck. +- Relays have to be controllable through a 3.3V DC signal. +- It is easier to find SSR supporting high loads that can be controlled by a 3.3V DC signal than electromagnetic relays. +- Also, SSR with a DIN Rail clip are easy to install. +- On the other hand, SSR can be more affected by harmonics than electromagnetic relays and they are more expensive. + +Here are some links where to fine pros and cons of each relay type: + +- [Solid State Relays, Types & Usage](https://www.sound-au.com/articles/ss-relays.htm) +- [Solid State Relay Guide](https://www.phidgets.com/docs/Solid_State_Relay_Guide) +- [https://www.celduc-relais.com/fr/thyristor-vs-triac/](https://www.celduc-relais.com/fr/thyristor-vs-triac/) +- [Fonctionnement du relais statique](https://www.geya.net/fr/solid-state-relay-working-how-does-a-solid-state-relay-work/) + +## Compatible ESP32 Boards + +The full list of ESP32 boards can be found [here](https://docs.platformio.org/en/stable/boards/index.html#espressif-32). +Here are the boards we know are compatible and those we have tested. + +| **Board** | **Compatible** | **Tested** | **Ethernet** | **Typical Name** | +| :----------------- | :------------: | :--------: | :----------: | :-------------------------------------------------------------------------------------------------------------------------------------- | +| esp32 | ✅ | ✅ | | [ESP32 NodeMCU Dev Kit C](https://docs.platformio.org/en/stable/boards/espressif32/esp32dev.html) | +| esp32s | ✅ | ✅ | | [ESP32S NodeMCU Dev Kit C](https://docs.platformio.org/en/stable/boards/espressif32/nodemcu-32s.html) | +| esp32c3 | ✅ | | | [Espressif ESP32-C3-DevKitC-02](https://docs.platformio.org/en/stable/boards/espressif32/esp32-c3-devkitc-02.html) | +| esp32s3 | ✅ | ✅ | | [Espressif ESP32-S3-DevKitC-1-N8 (8 MB QD, No PSRAM)](https://docs.platformio.org/en/stable/boards/espressif32/esp32-s3-devkitc-1.html) | +| d1_mini32 | ❌ | | | [WEMOS D1 MINI ESP32](https://docs.platformio.org/en/stable/boards/espressif32/wemos_d1_mini32.html) | +| lolin32_lite | ✅ | | | [WEMOS LOLIN32 Lite](https://docs.platformio.org/en/stable/boards/espressif32/lolin32_lite.html) | +| lolin_c3_mini | ❌ | | | [WEMOS LOLIN C3 Mini](https://docs.platformio.org/en/stable/boards/espressif32/lolin_c3_mini.html) | +| lolin_s2_mini | ✅ | | | [WEMOS LOLIN S2 Mini](https://docs.platformio.org/en/stable/boards/espressif32/lolin_s2_mini.html) | +| esp32_poe | ✅ | | ✅ | [OLIMEX ESP32-PoE](https://docs.platformio.org/en/stable/boards/espressif32/esp32-poe.html) | +| wt32_eth01 | ✅ | ✅ | ✅ | [Wireless-Tag WT32-ETH01 Ethernet Module](https://docs.platformio.org/en/stable/boards/espressif32/wt32-eth01.html) | +| lilygo_eth_lite_s3 | ✅ | ✅ | ✅ | [T-ETH-Lite ESP32 S3](https://www.lilygo.cc/products/t-eth-lite?variant=43120880779445) | +| m5stack-atom | ✅ | | | [M5Stack-ATOM](https://docs.platformio.org/en/stable/boards/espressif32/m5stack-atom.html) | +| m5stack-atoms3 | ✅ | | | [M5Stack AtomS3](https://docs.platformio.org/en/stable/boards/espressif32/m5stack-atoms3.html) | + +_Compatible_ means a firmware for this board can at least be built and flashed. +_Not Compatible_ means that the firmware cannot be built for this board or that the board has been unsuccessfully tested. + +_Tested_ means someone has verified that firmware is working or partially working on this board. +_No Tested_ means that we do not have the ability to test, but the board is at least compatible. + +## Compatible Hardware + +Here is the non exhaustive hardware that is compatible with YaS☀️lR firmware. +Links are provided for reference only, you can find them on other websites. + +YaS☀️lR supports many builds and routing algorithms. +To know what you need to buy, please read the Wiring section below to chose the right hardware depending on which router you want to build. + +| **ESP32 Boards (\*)** | Micro-controllers | +| :----------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| | ESP32 NodeMCU [Dev Kit C](https://www.az-delivery.de/en/products/esp32-developmentboard), [Dev Kit C](https://fr.aliexpress.com/item/1005005033683862.html), [Dev Kit C v4](https://fr.aliexpress.com/item/1005004936575415.html), [Dev Kit C v4](https://fr.aliexpress.com/item/1005005060818892.html) | +| | ESP-32S NodeMCU [ESP-32S](https://www.az-delivery.de/fr/products/nodemcu-esp-32s-kit), [ESP-32S](https://fr.aliexpress.com/item/1005005057893916.html), [ESP-32S/WROOM-32E/WROOM-32UE/WROVER-E/WROVER-IE](https://fr.aliexpress.com/item/1005004571486357.html) | +| | ESP32-S3 [N8R2/N16R8](https://fr.aliexpress.com/item/1005006266375800.html), [N16R8](https://fr.aliexpress.com/item/1005005078651330.html) | +| | [LILYGO T-ETH-Lite ESP32-S3](https://www.lilygo.cc/products/t-eth-lite) (Ethernet) | +| | [WT32-ETH01](https://fr.aliexpress.com/item/1005004436473683.html) v1.4 (Ethernet) | +| | [WiFi Pigtail Antenna](https://fr.aliexpress.com/item/32957527411.html) for ESP32 boards supporting external WiFi antenna | +| **Dimmers (\*)** | Dimmers are required and must be selected carefully depending on the load and the routing algorithms | +| | [Robodyn AC Dimmer 24A/600V](https://www.aliexpress.com/item/1005001965951718.html) Includes ZCD, supports **Phase Control** and **Burst mode** | +| | [Robodyn AC Dimmer 40A/800V](https://fr.aliexpress.com/item/1005006211999051.html) Includes ZCD, supports **Phase Control** and **Burst mode** | +| | Triac BTA40-800B RD91 [here](https://fr.aliexpress.com/item/32892486601.html) or [here](https://fr.aliexpress.com/item/1005001762265497.html) if you want / need to replace the Triac inside your Robodyn | +| | [LCTC Random Solid State Relay (SSR) that can be controlled by a 3.3V DC signal](https://www.aliexpress.com/item/1005004084038828.html), ([Other LCTC vendor link](https://fr.aliexpress.com/item/1005004863817921.html)). Supports **Phase Control** and **Burst mode**, See [How to choose your SSR ?](#how-to-choose-your-ssr-) below | +| | [Zero-Cross Solid State Relay (SSR) that can be controlled by a 3.3V DC signal](https://fr.aliexpress.com/item/1005003216482476.html) Supports **Burst mode**, See [How to choose your SSR ?](#how-to-choose-your-ssr-) below | +| | [Loncont LSA-H3P50YB](https://fr.aliexpress.com/item/32606780994.html) (also available in 70A and more). Includes ZCD, supports **Phase Control** and **Burst mode** | +| | [LCTC Voltage Regulator 220V / 40A](https://fr.aliexpress.com/item/1005005008018888.html) or [more models without heat sink but 60A, 80A, etc](https://fr.aliexpress.com/item/20000003997748.html) (also available in 70A and more). Includes ZCD, supports **Phase Control** and **Burst mode** | +| | [3.3V PWM Signal to 0-10V Convertor ](https://fr.aliexpress.com/item/1005004859012736.html) or [this link](https://fr.aliexpress.com/item/1005006859312414.html) or [this link](https://fr.aliexpress.com/item/1005007211285500.html) or [this link](https://fr.aliexpress.com/item/1005006822631244.html). Required to use the voltage regulators to convert the ESP32 pulse signal on 3.3V to an analogic output from 0-10V (external 12V power supply required) | +| **Zero-Cross Detection (\*)** | ZCD circuits are used to detect when the AC voltage crosses the 0V. It is either included in dimmers like Robodyn, or need to be added as an external device. This is required for a router to properly work and control the output power. | +| | [Very good ZCD module for DIN Rail from Daniel S.](https://www.pcbway.com/project/shareproject/Zero_Cross_Detector_a707a878.html) (used in conjunction with a Random SSR) | +| **Power and Energy Monitoring (\*)** | List of devices used to measure Grid Power and routed power. Measuring the Grid Power is required. | +| | [JSY-MK-194T with 1 fixed tore and 1 remote clamp](https://www.aliexpress.com/item/1005005396796284.html) Used to measure the grid power and total routed power | +| | [JSY-MK-194T with 2 remote clamps](https://fr.aliexpress.com/item/1005005529999366.html) Used to measure the grid power and total routed power | +| | Peacefair PZEM-004T V3 100A Openable (with clamp) [official](https://fr.aliexpress.com/item/33043137964.html), [with connector](https://fr.aliexpress.com/item/1005005984795952.html), [USB-TTL Cable](https://fr.aliexpress.com/item/1005006255175075.html). Can be used to measure each output individually and more precisely. Several PZEM-004T can be connected to the same Serial port. | +| | [Shelly EM](https://www.shelly.com/en-fr/products/product-overview/shelly-em-120a/shelly-em-2x-50a) (or any other alternative sending data to MQTT) | +| **Temperature Sensors** | Used to activate some router features such as auto-bypass and monitor the heating of the router box | +| | [DS18B20 Temperature Sensor + Adapter](https://fr.aliexpress.com/item/4000143479592.html) (easier to use to install in the water tank - take a long cable) | +| **Bypass and External Relay** | Used to activate some router features such as controlling other loads, and also efficiently bypass the dimmers when forcing a planned heating | +| | 1-Channel 5V DC / 30A Electromagnetic Relay on DIN Rail Support: [here](https://fr.aliexpress.com/item/1005004908430389.html), [here](https://fr.aliexpress.com/item/32999654399.html), [here](https://fr.aliexpress.com/item/1005005870389973.html), [here](https://fr.aliexpress.com/item/1005005883440249.html) | +| | 2-Channel 5V DC / 30A Dual Electromagnetic Relays on DIN Rail Support: [here](https://fr.aliexpress.com/item/1005004899369193.html), [here](https://fr.aliexpress.com/item/32999654399.html), [here](https://fr.aliexpress.com/item/1005005870389973.html), [here](https://fr.aliexpress.com/item/1005005883440249.html), [here](https://fr.aliexpress.com/item/1005001543232221.html) | +| | [Solid State Relay](https://fr.aliexpress.com/item/1005003216482476.html) (See [How to choose your SSR ?](#how-to-choose-your-ssr-) below) | +| **Screens** | Optionally add a screen | +| | [SSD1306 OLED Display 4 pins 128x64 I2C](https://www.aliexpress.com/item/32638662748.html) | +| | [SH1106 OLED Display 4 pins 128x64 I2C](https://www.aliexpress.com/item/1005001621782442.html) | +| | [SSD1307 OLED Display 4 pins 128x64 I2C](https://www.aliexpress.com/item/1005003297480376.html) | +| **Manual Control and Status** | Optionally add LEDs, Push button | +| | Push Buttons [Amazon](https://www.amazon.fr/dp/B0C2Y46BK6) (16mm) [AliExpress](https://fr.aliexpress.com/item/4001081212556.html) (12mm) for restart, manual bypass, reset | +| | Traffic lights Lights module for system status [AZ-Delivery](https://www.az-delivery.de/en/products/led-ampel-modul), [AliExpress](https://fr.aliexpress.com/item/32957515484.html) | +| **Heat Dissipation** | | +| | [Heat Sink for Random SSR and Triac](https://fr.aliexpress.com/item/1005004879389236.html) (there are many more types available: take a big heat sink placed vertically) | +| | [Heat Sink for SSR](https://fr.aliexpress.com/item/32739226601.html) (there are many more types available: take a big heat sink placed vertically) | +| | [Raspberry Fans](https://www.az-delivery.de/en/products/raspberry-pi-dc-burstenlose-lufter-kuhlkorper-kuhler-3-3-v-5-v) (for Robodyn AC dimmer) | +| **Mounting Accessories** | Some useful accessories to help mount components together | +| | [Electric Box](https://www.amazon.fr/gp/product/B0BWFGVV4S) | +| | [Extension boards](https://www.amazon.fr/dp/B0BCWBW4SR) (pay attention to the distance between header, there are different models. This one fits the ESP32 NodeMCU above) | +| | [DIN Rail Mount for PCB 72mm x 20mm](https://fr.aliexpress.com/item/32276247838.html) for the ZCD module above to mount on DIN Rail. [Alternative link](https://fr.aliexpress.com/item/4000272944733.html) | +| | [DIN Rail Mount for ESP32 NodeMCU Dev Kit C](https://fr.aliexpress.com/item/1005005096107275.html) | +| | [Distribution Module](https://fr.aliexpress.com/item/1005005996836930.html) / [More choice](https://fr.aliexpress.com/item/1005006039723013.html) | +| | [DIN Rail Clips for SSR](https://fr.aliexpress.com/item/1005004396715182.html) | +| | AC-DC 5V 2.4A DIN Adapter HDR-15-5 [Amazon](https://www.amazon.fr/Alimentation-rail-Mean-Well-HDR-15-5/dp/B06XWQSJGW), [AliExpress](https://fr.aliexpress.com/item/4000513120668.html). Can be used to power the ESP when installed in an electric box on DIN rail. Also if you need, a 12V version s available: [HDR-15-15 12V DC version](https://www.amazon.fr/Alimentation-rail-Mean-Well-HDR-15-5/dp/B07942GFTH?th=1) | +| | [3D Print enclosure for JSY-MK-194T](https://www.thingiverse.com/thing:6003867). You can screw it on an SSR DIN Rail to place your JSY on a DIN Rail in this enclosure. | +| **Wires** | Some useful accessories to help mount components together | +| | [Dupont Cable Kit](https://fr.aliexpress.com/item/1699285992.html) | +| | [100 ohms 0.1uF RC Snubber](https://www.quintium.fr/shelly/168-shelly-rc-snubber.html) (for Robodyn AC dimmer and Random SSR: can be placed at dimmer output) | + +**(\*)** Required items + +**IMPORTANT NOTES:** + +1. It is possible to switch the TRIAC of an original Robodyn AC Dimmer with a higher one, for example a [BTA40-800B BOITIER RD-91](https://fr.farnell.com/stmicroelectronics/bta40-800b/triac-40a-800v-boitier-rd-91/dp/9801731)
+ Ref: [Triacs gradateurs pour routeur photovoltaïque](https://f1atb.fr/fr/triac-gradateur-pour-routeur-photovoltaique/). + +2. The heat sink must be chosen according to the SSR / Triac. Here is a good video about the theory: [Calcul du dissipateur pour le triac d'un routeur](https://www.youtube.com/watch?v=_zAx1Q2IvJ8) (from Pierre) + +3. Make sure to [improve the Robodyn wiring/soldering](https://sites.google.com/view/le-professolaire/routeur-professolaire) + +### How to choose your SSR ? + +Solid State Relays can be used: + +- for routing the power to the load (either random or ZC) +- for Bypass and External Relays + +Things to consider: + +- Make sure you add a heat sink to the SSR or pick one with a heat sink, especially if you use a Random SSR instead of a Robodyn +- Type of control: DA: (DC Control AC) +- Control voltage: 3.3V should be in the range (example: 3-32V DC) +- Verify that the output AC voltage is in the range (example: 24-480V AC) +- Verify the SSR amperage: usually, it should be 2-3 times the nominal current of your resistive load (example: 40A SSR for a 3000W resistance) +- **Zero Cross SSR** (which is the default): for the bypass relay or external relays or when using Burst modulation routing algorithm +- **Random SSR**: if you chose to not use the RobodDyn but a Random SSR for Phase Control + +Other SSR: + +- [Zero-Cross SSR DA](https://fr.aliexpress.com/item/1005002297502716.html) +- [Zero-Cross SSR DA + Heat Sink + Din Rail Clip](https://www.aliexpress.com/item/1005002503185415.html) (40A, 60A, very high - can prevent closing an electric box) +- [Zero-Cross SSR 120 DA](https://www.aliexpress.com/item/1005005020709764.html) (for very high load) + +### Examples of Shopping Lists + +For each example below, you can add: + +- ESP32 NodeMCU Dev Kit C +- DIN Rail Mount for ESP32 NodeMCU Dev Kit C +- HDR-15-5 (to power the ESP) +- 1x or 2x 2-Channel 5V DC / 30A Dual Electromagnetic Relays on DIN Rail (for bypass relay + external relay) +- JSY (to measure the grid power and total routed power) +- 1x or 2x PZEM-004T (to measure each output individually and more precisely) +- Cables, LEDs, etc + +**Example 1: For loads up to 2000 W** + +- Robodyn 40A/800V (placed vertically) + +**Example 2: For loads up to 2000 W** + +- Vertical Heat Sink +- Robodyn 24A/600V (but we move the Triac on the Heat Sink above) + +**Example 3: For loads up to 3000 W** + +- Vertical Heat Sink +- Triac BTA40-800B RD91 (mounted on the heat sink) +- Robodyn 24A/600V (but we replace the Triac with the one above) + +**Example 4: For any load - SSR based** + +- Random Solid State Relay: rated 3x your load (i.e. 40DA for max 3000W) +- Heat Sink for SSR (matching your load, vertical ideally) +- DIN Rail clip for SSR +- ZCD module + DIN Rail mount + +## Default GPIO pinout per board + +The hardware and GPIO pinout are heavily inspired by [Routeur solaire PV monophasé Le Profes'Solaire](https://sites.google.com/view/le-professolaire/routeur-professolaire) from Anthony. +Please read all the information there first. +He did a very great job with some videos explaining the wiring. + +Most of the features can be enabled or disabled through the app and the GPIO pinout can be changed also trough the app. + +Here are below the default GPIO pinout for each board. + +**Tested boards:** + +| **FEATURE** | **ESP32** | **NodeMCU-32S** | **esp32s3** | **wt32_eth01** | **T-ETH-Lite** | +| :-------------------------------- | :-------: | :-------------: | :---------: | :------------: | :------------: | +| Display CLOCK (CLK) | 22 | 22 | 9 | 32 | 40 | +| Display DATA (SDA) | 21 | 21 | 8 | 33 | 41 | +| JSY-MK-194T RX (Serial TX) | 17 | 17 | 17 | 17 | 17 | +| JSY-MK-194T TX (Serial RX) | 16 | 16 | 16 | 5 | 18 | +| Light Feedback (Green) | 0 | 0 | 0 | -1 | 38 | +| Light Feedback (Red) | 15 | 15 | 15 | -1 | 46 | +| Light Feedback (Yellow) | 2 | 2 | 2 | -1 | 21 | +| OUTPUT #1 Bypass Relay | 32 | 32 | 40 | 12 | 20 | +| OUTPUT #1 Dimmer (Robodyn or SSR) | 25 | 25 | 37 | 2 | 19 | +| OUTPUT #1 Temperature Sensor | 18 | 18 | 18 | 15 | 3 | +| OUTPUT #2 Bypass Relay | 33 | 33 | 33 | -1 | 15 | +| OUTPUT #2 Dimmer (Robodyn or SSR) | 26 | 26 | 36 | -1 | 7 | +| OUTPUT #2 Temperature Sensor | 5 | 5 | 5 | -1 | 16 | +| Push Button (restart) | EN | EN | EN | EN | EN | +| RELAY #1 | 13 | 13 | 13 | 14 | 5 | +| RELAY #2 | 12 | 12 | 12 | -1 | 6 | +| System Temperature Sensor | 4 | 4 | 4 | 4 | 4 | +| ZCD (Robodyn or ZCD Sync) | 35 | 35 | 35 | 35 | 8 | +| PZEM-004T v3 RX (Serial TX) | 27 | 27 | 11 | -1 | -1 | +| PZEM-004T v3 TX (Serial RX) | 14 | 14 | 14 | -1 | -1 | + +- `-1` means not mapped (probably because the board does not have enough pins) + +**Potential compatible boards, but not tested yet:** + +| **FEATURE** | **esp32-poe** | **ESP32-C3-DevKitC-02** | **lolin32_lite** | **lolin_s2_mini** | **m5stack-atom** | **m5stack-atoms3** | +| :-------------------------------- | :-----------: | :---------------------: | :--------------: | :---------------: | :--------------: | :----------------: | +| Display CLOCK (CLK) | 16 | 6 | 22 | 9 | -1 | -1 | +| Display DATA (SDA) | 13 | 7 | 19 | 8 | -1 | -1 | +| JSY-MK-194T RX (Serial TX) | 33 | 20 | 17 | 39 | -1 | -1 | +| JSY-MK-194T TX (Serial RX) | 35 | 21 | 16 | 37 | -1 | -1 | +| Light Feedback (Green) | -1 | -1 | 0 | 3 | -1 | -1 | +| Light Feedback (Red) | -1 | -1 | 15 | 6 | -1 | -1 | +| Light Feedback (Yellow) | -1 | -1 | 2 | 2 | -1 | -1 | +| OUTPUT #1 Bypass Relay | 4 | 2 | 32 | 21 | -1 | -1 | +| OUTPUT #1 Dimmer (Robodyn or SSR) | 2 | 1 | 25 | 10 | -1 | -1 | +| OUTPUT #1 Temperature Sensor | 5 | 0 | 18 | 18 | -1 | -1 | +| OUTPUT #2 Bypass Relay | -1 | 9 | 33 | 33 | -1 | -1 | +| OUTPUT #2 Dimmer (Robodyn or SSR) | -1 | 8 | 26 | 11 | -1 | -1 | +| OUTPUT #2 Temperature Sensor | -1 | 5 | 5 | 5 | -1 | -1 | +| Push Button (restart) | EN | EN | EN | EN | EN | EN | +| RELAY #1 | 14 | -1 | 13 | 13 | -1 | -1 | +| RELAY #2 | 15 | -1 | 12 | 12 | -1 | -1 | +| System Temperature Sensor | 0 | 4 | 4 | 4 | -1 | -1 | +| ZCD (Robodyn or ZCD Sync) | 36 | 3 | 35 | 35 | -1 | -1 | +| PZEM-004T v3 RX (Serial TX) | -1 | -1 | -1 | -1 | -1 | -1 | +| PZEM-004T v3 TX (Serial RX) | -1 | -1 | -1 | -1 | -1 | -1 | + +- `-1` means not mapped (probably because the board does not have enough pins) +- This mapping table might contain errors + +**Minimal requirements:** + +- a pin configured to the ZCD system: either the ZC pin of the Robodyn or any pin from any other ZC detection module +- a pin configured to the Phase Control system: PSM pin for the Robodyn or DC + side of the Random SSR + +The website display the pinout configured, the pinout layout that is live at runtime and also displays some potential issues like duplicate pins or wrong pin configuration. + +[![](./assets/img/screenshots/pinout_configured.jpeg)](./assets/img/screenshots/pinout_configured.jpeg) + +## Pictures of some routers + +> _TO BE COMPLETED_ diff --git a/docs/buy.md b/docs/buy.md new file mode 100644 index 0000000..b3f80a3 --- /dev/null +++ b/docs/buy.md @@ -0,0 +1,54 @@ +--- +layout: default +title: Buy +description: Buy +--- + +# Buy YaS☀️lR Pro + +OSS and Pro firmware are the same, except that the PRO version relies on commercial (paid) libraries and provides some additional features based on a better dashboard. + +**The Pro version is only 25 euros** and gives access to all the perks of the Pro version below: + +| Feature | OSS (Free) | PRO (Paid) | +| -------------------------- | :--------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +| Dashboard | Overview **only** | Full Dashboard as seen in the screenshots | +| Manual Dimmer Control | Home Assistant
MQTT API
REST API | **From Dashboard**
Home Assistant
MQTT API
REST API | +| Manual Bypass Control | Home Assistant
MQTT API
REST API | **From Dashboard**
Home Assistant
MQTT API
REST API | +| Manual Relay Control | Home Assistant
MQTT API
REST API | **From Dashboard**
Home Assistant
MQTT API
REST API | +| Configuration | Debug Config Page | **From Dashboard**
Debug Config Page | +| Health View from Dashboard | ❌ | ✅ | +| Statistics and Charts | ❌ | ✅ | +| PZEM Pairing | ❌ | ✅ | +| Help & Support | [Facebook Group](https://www.facebook.com/groups/yasolr) | [Facebook Group](https://www.facebook.com/groups/yasolr)
[Forum](https://github.com/mathieucarbou/YaSolR-OSS/discussions)
[Bug Report](https://github.com/mathieucarbou/YaSolR-OSS/issues) | +| Web Console | [WebSerial Lite](https://github.com/mathieucarbou/WebSerialLite) | [WebSerial Pro](https://www.webserial.pro) | +| Dashboard | [ESP-DASH](https://github.com/ayushsharma82/ESP-DASH) | [ESP-DASH Pro](https://espdash.pro) | +| OTA Firmware Update | [ElegantOTA](https://github.com/ayushsharma82/ElegantOTA) | [ElegantOTA Pro](https://elegantota.pro) | + +The money helps funding the hardware necessary to test and develop the firmware. + +## How to buy: + +**WARNING! THE PROJECT IS STILL IN DEVELOPMENT: All the features marked with 🚧 in the home page are not yet available** + +1. Get a **[Github](https://github.com/)** account so that I can add your GitHub username to the project repository from where you can download all the firmware files. + +2. Make a donation of **25 euros or more** (through [Github](https://github.com/sponsors/mathieucarbou) or [Paypal](https://www.paypal.com/donate/?hosted_button_id=QJYRAPXGEDCNS)). + Any sponsoring of 25 euros or more will give access to the **Pro version and all the upcoming updates for an unlimited time**! + +Notes: + +- Github is the preferred way to sponsor +- If you prefer Paypal, do not forget to add your GitHub username in the Paypal form (there will be a comment / note field for that). + +Thanks a lot! + +# Sponsoring + +Any sponsoring is greatly appreciated to help me continue working in this project and all other project I maintain (see [all the Open-Source projects and Arduino / ESP32 libraries I have created](https://oss.carbou.me)). + +Here are 2 ways to sponsor: + +| **[Using GitHub](https://github.com/sponsors/mathieucarbou)
(Preferred way)** | **[Using Paypal](https://www.paypal.com/donate/?hosted_button_id=QJYRAPXGEDCNS)** | +| :--------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------: | +| [![](assets/img/Github_Donate.png)](assets/img/Github_Donate.png) | [![](assets/img/Paypal_Donate.png)](assets/img/Paypal_Donate.png) | diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-esp32-en.FACTORY.bin b/docs/docs/downloads/trials/YaSolR-main-trial-esp32-en.FACTORY.bin new file mode 100644 index 0000000..bad5c44 Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-esp32-en.FACTORY.bin differ diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-esp32-en.UPDATE.bin b/docs/docs/downloads/trials/YaSolR-main-trial-esp32-en.UPDATE.bin new file mode 100644 index 0000000..f5859fd Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-esp32-en.UPDATE.bin differ diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-esp32-fr.FACTORY.bin b/docs/docs/downloads/trials/YaSolR-main-trial-esp32-fr.FACTORY.bin new file mode 100644 index 0000000..c916c5f Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-esp32-fr.FACTORY.bin differ diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-esp32-fr.UPDATE.bin b/docs/docs/downloads/trials/YaSolR-main-trial-esp32-fr.UPDATE.bin new file mode 100644 index 0000000..b645069 Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-esp32-fr.UPDATE.bin differ diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-esp32s-en.FACTORY.bin b/docs/docs/downloads/trials/YaSolR-main-trial-esp32s-en.FACTORY.bin new file mode 100644 index 0000000..55cb7e6 Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-esp32s-en.FACTORY.bin differ diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-esp32s-en.UPDATE.bin b/docs/docs/downloads/trials/YaSolR-main-trial-esp32s-en.UPDATE.bin new file mode 100644 index 0000000..b615784 Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-esp32s-en.UPDATE.bin differ diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-esp32s-fr.FACTORY.bin b/docs/docs/downloads/trials/YaSolR-main-trial-esp32s-fr.FACTORY.bin new file mode 100644 index 0000000..e32361a Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-esp32s-fr.FACTORY.bin differ diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-esp32s-fr.UPDATE.bin b/docs/docs/downloads/trials/YaSolR-main-trial-esp32s-fr.UPDATE.bin new file mode 100644 index 0000000..3a791f1 Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-esp32s-fr.UPDATE.bin differ diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-esp32s3-en.FACTORY.bin b/docs/docs/downloads/trials/YaSolR-main-trial-esp32s3-en.FACTORY.bin new file mode 100644 index 0000000..49bc338 Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-esp32s3-en.FACTORY.bin differ diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-esp32s3-en.UPDATE.bin b/docs/docs/downloads/trials/YaSolR-main-trial-esp32s3-en.UPDATE.bin new file mode 100644 index 0000000..e51293d Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-esp32s3-en.UPDATE.bin differ diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-esp32s3-fr.FACTORY.bin b/docs/docs/downloads/trials/YaSolR-main-trial-esp32s3-fr.FACTORY.bin new file mode 100644 index 0000000..f26c3e2 Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-esp32s3-fr.FACTORY.bin differ diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-esp32s3-fr.UPDATE.bin b/docs/docs/downloads/trials/YaSolR-main-trial-esp32s3-fr.UPDATE.bin new file mode 100644 index 0000000..9b0a2ae Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-esp32s3-fr.UPDATE.bin differ diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-en.FACTORY.bin b/docs/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-en.FACTORY.bin new file mode 100644 index 0000000..3ad8d1a Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-en.FACTORY.bin differ diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-en.UPDATE.bin b/docs/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-en.UPDATE.bin new file mode 100644 index 0000000..531b563 Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-en.UPDATE.bin differ diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-fr.FACTORY.bin b/docs/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-fr.FACTORY.bin new file mode 100644 index 0000000..cf51414 Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-fr.FACTORY.bin differ diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-fr.UPDATE.bin b/docs/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-fr.UPDATE.bin new file mode 100644 index 0000000..186b7fc Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-fr.UPDATE.bin differ diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-en.FACTORY.bin b/docs/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-en.FACTORY.bin new file mode 100644 index 0000000..bb22052 Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-en.FACTORY.bin differ diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-en.UPDATE.bin b/docs/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-en.UPDATE.bin new file mode 100644 index 0000000..d38e162 Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-en.UPDATE.bin differ diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-fr.FACTORY.bin b/docs/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-fr.FACTORY.bin new file mode 100644 index 0000000..e865ac2 Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-fr.FACTORY.bin differ diff --git a/docs/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-fr.UPDATE.bin b/docs/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-fr.UPDATE.bin new file mode 100644 index 0000000..d936304 Binary files /dev/null and b/docs/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-fr.UPDATE.bin differ diff --git a/docs/download.md b/docs/download.md new file mode 100644 index 0000000..bbc1b21 --- /dev/null +++ b/docs/download.md @@ -0,0 +1,61 @@ +--- +layout: default +title: Download +description: Download +--- + +# YaS☀️lR Downloads + +**Please make sure to download the firmware matching your board.** + +Firmware files are named as follow: + +- `YaSolR----.UPDATE.bin`: This firmware is used to update through the Web OTA interface +- `YaSolR----.FACTORY.bin`: This firmware is used for a first ESP installation, or wen doing a factory reset through USB flashing + +Where: + +- `VERSION`: YaS☀️lR version, or `main` for the latest development build +- `MODEL`: `oss`, `pro` +- `BOARD`: the board name, from the list of [Compatible ESP32 boards](/build#compatible-esp32-boards) +- `LANG`: `en`, `fr`, ... + +## Open-Source versions + +- [latest](https://github.com/mathieucarbou/YaSolR-OSS/releases/tag/latest) (latest development build can be unstable) + +_Firmware and source code for the Open-Source version are available directly in the GitHub project at [https://github.com/mathieucarbou/YaSolR-OSS/releases](https://github.com/mathieucarbou/YaSolR-OSS/releases)._ + +## Pro versions + +- [latest](https://github.com/mathieucarbou/YaSolR-Pro/tree/main/latest) (latest development build can be unstable) + +_Firmware for the Pro version are only available to Pro users. You must be logged into your GitHub account to access them._ + +Please go to the [Buy](buy) page if you are interested in buying the Pro version. + +### Trial versions + +- [YaSolR-main-trial-esp32-en.FACTORY.bin](/downloads/trials/YaSolR-main-trial-esp32-en.FACTORY.bin) +- [YaSolR-main-trial-esp32-en.UPDATE.bin](/downloads/trials/YaSolR-main-trial-esp32-en.UPDATE.bin) +- [YaSolR-main-trial-esp32-fr.FACTORY.bin](/downloads/trials/YaSolR-main-trial-esp32-fr.FACTORY.bin) +- [YaSolR-main-trial-esp32-fr.UPDATE.bin](/downloads/trials/YaSolR-main-trial-esp32-fr.UPDATE.bin) +- [YaSolR-main-trial-esp32s-en.FACTORY.bin](/downloads/trials/YaSolR-main-trial-esp32s-en.FACTORY.bin) +- [YaSolR-main-trial-esp32s-en.UPDATE.bin](/downloads/trials/YaSolR-main-trial-esp32s-en.UPDATE.bin) +- [YaSolR-main-trial-esp32s-fr.FACTORY.bin](/downloads/trials/YaSolR-main-trial-esp32s-fr.FACTORY.bin) +- [YaSolR-main-trial-esp32s-fr.UPDATE.bin](/downloads/trials/YaSolR-main-trial-esp32s-fr.UPDATE.bin) +- [YaSolR-main-trial-esp32s3-en.FACTORY.bin](/downloads/trials/YaSolR-main-trial-esp32s3-en.FACTORY.bin) +- [YaSolR-main-trial-esp32s3-en.UPDATE.bin](/downloads/trials/YaSolR-main-trial-esp32s3-en.UPDATE.bin) +- [YaSolR-main-trial-esp32s3-fr.FACTORY.bin](/downloads/trials/YaSolR-main-trial-esp32s3-fr.FACTORY.bin) +- [YaSolR-main-trial-esp32s3-fr.UPDATE.bin](/downloads/trials/YaSolR-main-trial-esp32s3-fr.UPDATE.bin) +- [YaSolR-main-trial-lilygo_eth_lite_s3-en.FACTORY.bin](/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-en.FACTORY.bin) +- [YaSolR-main-trial-lilygo_eth_lite_s3-en.UPDATE.bin](/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-en.UPDATE.bin) +- [YaSolR-main-trial-lilygo_eth_lite_s3-fr.FACTORY.bin](/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-fr.FACTORY.bin) +- [YaSolR-main-trial-lilygo_eth_lite_s3-fr.UPDATE.bin](/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-fr.UPDATE.bin) +- [YaSolR-main-trial-wt32_eth01-en.FACTORY.bin](/downloads/trials/YaSolR-main-trial-wt32_eth01-en.FACTORY.bin) +- [YaSolR-main-trial-wt32_eth01-en.UPDATE.bin](/downloads/trials/YaSolR-main-trial-wt32_eth01-en.UPDATE.bin) +- [YaSolR-main-trial-wt32_eth01-fr.FACTORY.bin](/downloads/trials/YaSolR-main-trial-wt32_eth01-fr.FACTORY.bin) +- [YaSolR-main-trial-wt32_eth01-fr.UPDATE.bin](/downloads/trials/YaSolR-main-trial-wt32_eth01-fr.UPDATE.bin) + +_Trial versions are like the Pro version but will stop after **3 days of uptime**._ +_If you need to extend your trial period: you can re-flash the factory firmware after erasing the flash, put your settings back, and you will be good for another 3 days of trial._ diff --git a/docs/downloads/solar_diverter_v1.js b/docs/downloads/solar_diverter_v1.js new file mode 100644 index 0000000..5d24c7e --- /dev/null +++ b/docs/downloads/solar_diverter_v1.js @@ -0,0 +1,239 @@ +// SPDX-License-Identifier: MIT +/* + * Copyright (C) 2023-2024 Mathieu Carbou + * + * ====================================== + * CHANGELOG + * + * - v1: Initial version + * + * ====================================== + */ +const scriptName = "solar_diverter.js"; + +// Config + +const CONFIG = { + // Debug mode + DEBUG: 1, + // Grid Power Read Interval (s) + READ_INTERVAL_S: 1, + PID: { + // Target Grid Power (W) + SET_POINT: 0, + // Number of Watts allowed to be above or below the set point (W) + SET_POINT_DELTA: 0, + // PID Proportional Gain + KP: 0.8, + // PID Integral Gain + KI: 0.01, + // PID Derivative Gain + KD: 0.8, + // PID Output Minimum Clamp (W) + ITERM_MIN: -10, + }, + DIMMERS: { + "192.168.125.93": { + // Resistance (in Ohm) of the load connecter to the dimmer + voltage regulator + // 0 will disable the dimmer + RESISTANCE: 24, + // Percentage of the remaining excess power that will be assigned to this dimmer + // The remaining percentage will be given to the next dimmers + RESERVED_EXCESS_PERCENT: 100 + }, + "192.168.125.97": { + RESISTANCE: 0, + RESERVED_EXCESS_PERCENT: 100 + } + } +}; + +// PID Controller + +let PID = { + // PID Input + input: 0, + // PID Output + output: 0, + // current error value + error: 0, + // Proportional Term + pTerm: 0, + // Integral Term + iTerm: 0, + // Derivative Term + dTerm: 0, +} + +// Divert Control + +let DIVERT = { + lastTime: 0, + voltage: 0, + gridPower: CONFIG.PID.SET_POINT, + divertPower: CONFIG.PID.SET_POINT, + dimmers: {} +} + +function validateConfig(cb) { + print(scriptName, ":", "Validating Config...") + + if (CONFIG.DIMMERS.length === 0) { + print(scriptName, ":", "ERR: No dimmer configured"); + return; + } + + for (const ip in CONFIG.DIMMERS) { + if (CONFIG.DIMMERS[ip].RESISTANCE < 0) { + print(scriptName, ":", "ERR: Dimmer resistance should be greater than 0"); + return; + } + + if (CONFIG.DIMMERS[ip].RESISTANCE === 0) { + print(scriptName, ":", "Dimmer", ip, "is disabled"); + continue; + } + + print(scriptName, ":", "Dimmer", ip, "is enabled"); + DIVERT.dimmers[ip] = { + divertPower: 0 + } + } + + cb(); +} + +function calculatePID(input) { + PID.input = input; + const error = CONFIG.PID.SET_POINT - PID.input; + if (Math.abs(error) <= CONFIG.PID.SET_POINT_DELTA) { + PID.pTerm = 0; + PID.iTerm = 0; + PID.dTerm = 0; + PID.output = 0; + } else { + PID.pTerm = error * CONFIG.PID.KP; + PID.iTerm = Math.max(PID.iTerm + error * CONFIG.PID.KI, CONFIG.PID.ITERM_MIN); + PID.dTerm = (error - PID.error) * CONFIG.PID.KD; + } + PID.output = PID.pTerm + PID.iTerm + PID.dTerm; + PID.error = error; + return PID.output; +} + +function callDimmers(cb) { + for (const ip in DIVERT.dimmers) { + const dimmer = DIVERT.dimmers[ip]; + + // ignore contacted dimmers + if (dimmer.rpc !== "pending") { + continue; + } + + // build url + const url = "http://" + ip + "/rpc/Light.Set?id=0&on=" + (dimmer.dutyCycle > 0 ? "true" : "false") + "&brightness=" + (dimmer.dutyCycle * 100) + "&transition_duration=0.5"; + + if (CONFIG.DEBUG > 1) + print(scriptName, ":", "Calling Dimmer: ", url); + + // call dimmer + Shelly.call("HTTP.GET", { url: url, timeout: 5 }, function (result, err) { + if (err) { + print(scriptName, ":", "ERR:", err); + dimmer.rpc = "failed"; + } else if (result.code !== 200) { + const rpcResult = JSON.parse(result.body); + print(scriptName, ":", "ERR", rpcResult.code, ":", rpcResult.message); + dimmer.rpc = "failed"; + } else { + dimmer.rpc = "success"; + } + + // once done, call ourself again until no dimmer is left + callDimmers(cb); + }); + + // exit the loop immediately to avoid multiple calls in case the yare made in parallel + return; + } + + // if we are here, all dimmers have been contacted + cb(); +} + +function divert(voltage, gridPower) { + DIVERT.voltage = voltage; + DIVERT.gridPower = gridPower; + const correction = calculatePID(DIVERT.gridPower); + DIVERT.divertPower = Math.max(0, DIVERT.divertPower + correction); + + if (CONFIG.DEBUG > 0) + print(scriptName, ":", "Grid:", voltage, "V,", gridPower, "W. Correction:", correction, "W. Total Divert Power:", DIVERT.divertPower, "W"); + + let remaining = DIVERT.divertPower; + + for (const ip in DIVERT.dimmers) { + const dimmer = DIVERT.dimmers[ip]; + dimmer.nominalPower = voltage * voltage / CONFIG.DIMMERS[ip].RESISTANCE; + dimmer.divertPower = Math.min(remaining * CONFIG.DIMMERS[ip].RESERVED_EXCESS_PERCENT / 100, dimmer.nominalPower); + dimmer.dutyCycle = dimmer.divertPower / dimmer.nominalPower; + dimmer.powerFactor = Math.sqrt(dimmer.dutyCycle); + dimmer.dimmedVoltage = dimmer.powerFactor * voltage; + dimmer.current = dimmer.dimmedVoltage / CONFIG.DIMMERS[ip].RESISTANCE; + dimmer.apparentPower = dimmer.current * voltage; + dimmer.thdi = dimmer.dutyCycle === 0 ? 0 : Math.sqrt(1 / dimmer.dutyCycle - 1); + dimmer.rpc = "pending"; + + remaining -= dimmer.divertPower; + + if (CONFIG.DEBUG > 0) + print(scriptName, ":", "Dimmer", ip, "=>", dimmer.divertPower, "W"); + } + + callDimmers(throttleReadPower); +} + +function onEM1GetStatus(result, err) { + if (err) + return; + if (CONFIG.DEBUG > 1) + print(scriptName, ":", "EM1.GetStatus:", JSON.stringify(result)); + divert(result.voltage, result.act_power); +} + +function readPower() { + DIVERT.lastTime = Date.now(); + Shelly.call("EM1.GetStatus", { id: 0 }, onEM1GetStatus); +} + +function throttleReadPower() { + const now = Date.now(); + const diff = now - DIVERT.lastTime; + if (diff > 1000) { + readPower(); + } else { + Timer.set(1000 - diff, false, readPower); + } +} + +// HTTP handlers + +function onGetStatus(request, response) { + response.code = 200; + response.headers = { + "Content-Type": "application/json" + } + response.body = JSON.stringify({ + pid: PID, + divert: DIVERT + }); + response.send(); +} + +// Main + +validateConfig(function () { + print(scriptName, ":", "Starting Shelly Solar Diverter...") + HTTPServer.registerEndpoint("status", onGetStatus); + readPower(); +}); diff --git a/docs/downloads/trials/YaSolR-main-trial-esp32-en.FACTORY.bin b/docs/downloads/trials/YaSolR-main-trial-esp32-en.FACTORY.bin new file mode 100644 index 0000000..bad5c44 Binary files /dev/null and b/docs/downloads/trials/YaSolR-main-trial-esp32-en.FACTORY.bin differ diff --git a/docs/downloads/trials/YaSolR-main-trial-esp32-en.UPDATE.bin b/docs/downloads/trials/YaSolR-main-trial-esp32-en.UPDATE.bin new file mode 100644 index 0000000..f5859fd Binary files /dev/null and b/docs/downloads/trials/YaSolR-main-trial-esp32-en.UPDATE.bin differ diff --git a/docs/downloads/trials/YaSolR-main-trial-esp32-fr.FACTORY.bin b/docs/downloads/trials/YaSolR-main-trial-esp32-fr.FACTORY.bin new file mode 100644 index 0000000..c916c5f Binary files /dev/null and b/docs/downloads/trials/YaSolR-main-trial-esp32-fr.FACTORY.bin differ diff --git a/docs/downloads/trials/YaSolR-main-trial-esp32-fr.UPDATE.bin b/docs/downloads/trials/YaSolR-main-trial-esp32-fr.UPDATE.bin new file mode 100644 index 0000000..b645069 Binary files /dev/null and b/docs/downloads/trials/YaSolR-main-trial-esp32-fr.UPDATE.bin differ diff --git a/docs/downloads/trials/YaSolR-main-trial-esp32s-en.FACTORY.bin b/docs/downloads/trials/YaSolR-main-trial-esp32s-en.FACTORY.bin new file mode 100644 index 0000000..55cb7e6 Binary files /dev/null and b/docs/downloads/trials/YaSolR-main-trial-esp32s-en.FACTORY.bin differ diff --git a/docs/downloads/trials/YaSolR-main-trial-esp32s-en.UPDATE.bin b/docs/downloads/trials/YaSolR-main-trial-esp32s-en.UPDATE.bin new file mode 100644 index 0000000..b615784 Binary files /dev/null and b/docs/downloads/trials/YaSolR-main-trial-esp32s-en.UPDATE.bin differ diff --git a/docs/downloads/trials/YaSolR-main-trial-esp32s-fr.FACTORY.bin b/docs/downloads/trials/YaSolR-main-trial-esp32s-fr.FACTORY.bin new file mode 100644 index 0000000..e32361a Binary files /dev/null and b/docs/downloads/trials/YaSolR-main-trial-esp32s-fr.FACTORY.bin differ diff --git a/docs/downloads/trials/YaSolR-main-trial-esp32s-fr.UPDATE.bin b/docs/downloads/trials/YaSolR-main-trial-esp32s-fr.UPDATE.bin new file mode 100644 index 0000000..3a791f1 Binary files /dev/null and b/docs/downloads/trials/YaSolR-main-trial-esp32s-fr.UPDATE.bin differ diff --git a/docs/downloads/trials/YaSolR-main-trial-esp32s3-en.FACTORY.bin b/docs/downloads/trials/YaSolR-main-trial-esp32s3-en.FACTORY.bin new file mode 100644 index 0000000..49bc338 Binary files /dev/null and b/docs/downloads/trials/YaSolR-main-trial-esp32s3-en.FACTORY.bin differ diff --git a/docs/downloads/trials/YaSolR-main-trial-esp32s3-en.UPDATE.bin b/docs/downloads/trials/YaSolR-main-trial-esp32s3-en.UPDATE.bin new file mode 100644 index 0000000..e51293d Binary files /dev/null and b/docs/downloads/trials/YaSolR-main-trial-esp32s3-en.UPDATE.bin differ diff --git a/docs/downloads/trials/YaSolR-main-trial-esp32s3-fr.FACTORY.bin b/docs/downloads/trials/YaSolR-main-trial-esp32s3-fr.FACTORY.bin new file mode 100644 index 0000000..f26c3e2 Binary files /dev/null and b/docs/downloads/trials/YaSolR-main-trial-esp32s3-fr.FACTORY.bin differ diff --git a/docs/downloads/trials/YaSolR-main-trial-esp32s3-fr.UPDATE.bin b/docs/downloads/trials/YaSolR-main-trial-esp32s3-fr.UPDATE.bin new file mode 100644 index 0000000..9b0a2ae Binary files /dev/null and b/docs/downloads/trials/YaSolR-main-trial-esp32s3-fr.UPDATE.bin differ diff --git a/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-en.FACTORY.bin b/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-en.FACTORY.bin new file mode 100644 index 0000000..3ad8d1a Binary files /dev/null and b/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-en.FACTORY.bin differ diff --git a/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-en.UPDATE.bin b/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-en.UPDATE.bin new file mode 100644 index 0000000..531b563 Binary files /dev/null and b/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-en.UPDATE.bin differ diff --git a/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-fr.FACTORY.bin b/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-fr.FACTORY.bin new file mode 100644 index 0000000..cf51414 Binary files /dev/null and b/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-fr.FACTORY.bin differ diff --git a/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-fr.UPDATE.bin b/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-fr.UPDATE.bin new file mode 100644 index 0000000..186b7fc Binary files /dev/null and b/docs/downloads/trials/YaSolR-main-trial-lilygo_eth_lite_s3-fr.UPDATE.bin differ diff --git a/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-en.FACTORY.bin b/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-en.FACTORY.bin new file mode 100644 index 0000000..bb22052 Binary files /dev/null and b/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-en.FACTORY.bin differ diff --git a/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-en.UPDATE.bin b/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-en.UPDATE.bin new file mode 100644 index 0000000..d38e162 Binary files /dev/null and b/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-en.UPDATE.bin differ diff --git a/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-fr.FACTORY.bin b/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-fr.FACTORY.bin new file mode 100644 index 0000000..e865ac2 Binary files /dev/null and b/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-fr.FACTORY.bin differ diff --git a/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-fr.UPDATE.bin b/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-fr.UPDATE.bin new file mode 100644 index 0000000..d936304 Binary files /dev/null and b/docs/downloads/trials/YaSolR-main-trial-wt32_eth01-fr.UPDATE.bin differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..983d795 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,261 @@ +--- +layout: default +title: Home +description: Home +--- + +# What is YaS☀️lR ? + +YaS☀️lR is an Arduino / ESP32 firmware for Solar Routers, compatible with most of existing and easy to source hardware components. + +If you already have a Solar Router at home based on ESP32, built by yourself or someone else, there is a good chance that YaS☀️lR will be compatible. + +| [![](./assets/img/screenshots/overview.jpeg)](./assets/img/screenshots/overview.jpeg) | [![](./assets/img/screenshots/output1.jpeg)](./assets/img/screenshots/output1.jpeg) | + +**What YaS☀️lR is not?** + +YaS☀️lR **is not** a product and is not packaged with any hardware. +YaS☀️lR is **only** the software layer that will run on the Solar Router of your choice that you will have to build (or already have). + +## Why YaS☀️lR ? + +_YaSolR_ is built with this vision in mind: + +- **OpenSource**: anybody can help and participate in the project + +- **Good development practices**, PlatformIO project with versioning, CI, issue management, etc. + This is big project and not a simple Arduino project (.ino file) + +- **Hardware compatibility**: supports several phase control systems like Random SSR, Robodyn, many ESP32 boards with WiFi and Ethernet + +- **Flexible**: it is made in a way that you can only pick the components you need depending on the features you want to activate + +- **Easy to use**: The firmware should be easy to install and use + +- **Harmonic Regulations**: The firmware gives the user the ability to monitor and limit the router in order to stay below the levels of harmonic regulations + +- **110/230V 50/60Hz**: the dimmer implementation and proposed hardware all support both frequencies and voltages + +- **3-Phase** support + +- **Statistics, API, Home Automation**: Web API, MQTT API, Home Assistant auto-discovery, etc. + The router exposes a lot of statistics and information through MQTT and REST API and provides a very good integration with Home Assistant or other home automation systems. + +- **Optimized, Fast and Powerful**: the router logic is optimized to use the ESP32 hardware to its full potential and also react as fast as possible to the measurements, which are also taken in a dedicated loop in a few milliseconds. + - Own optimized libraries to read hardware components (JSY, PPZEM) + - JSY reading speed is increased to its maximum to give a fast reading + - RMT peripheral is used for DS18 readings + - PID Controller for dimmers with customizable Kp, KI, kd factors + - Optimized dimmer library (🚧) + - Motor Control Pulse Width Modulator (MCPWM) for phase control (🚧) + +## YaS☀️lR Features + +(_🚧_ means _In Progress_) + +- [2x Routing Outputs](#2x-routing-outputs) + - [Dimmer (required)](#dimmer-required) + - [Bypass Relay (optional)](#bypass-relay-optional) + - [Temperature Sensor (optional)](#temperature-sensor-optional) + - [Measurement device (optional)](#measurement-device-optional) + - [Additional Features](#additional-features) +- [Grid Power Measurement](#grid-power-measurement) + - [JSY-MK-194T](#jsy-mk-194t) + - [Remote Grid Measurement](#remote-grid-measurement) + - [3-Phase Support](#3-phase-support) +- [2x Relays](#2x-relays) +- [Monitoring and Management](#monitoring-and-management) +- [MQTT, REST API and Home Automation Systems](#mqtt-rest-api-and-home-automation-systems) +- [Networking / Offline](#networking--offline) +- [PID Control and Tuning](#pid-control-and-tuning) +- [Remote Capabilities](#remote-capabilities) +- [Virtual Excess and EV Charger Compatibility](#virtual-excess-and-ev-charger-compatibility) +- [OSS vs PRO](#oss-vs-pro) + +### 2x Routing Outputs + +A routing output is connected to a resistive load and controls its power by dimming the voltage. Each output is composed of the following components. + +#### Dimmer (required) + +Controls the power sent to the load. Example of supported dimmers: + +| Dimmer Type | `Phase Control` | `Burst Fire Control` (🚧) | +| :---------------------------------------------------------------------------------------- | :-------------: | :-----------------------: | +| **Robodyn 24A** ![Robodyn 24A](./assets/img/hardware/Robodyn_24A.jpeg) | ✅ | ✅ | +| **Robodyn 40A** ![Robodyn 40A](./assets/img/hardware/Robodyn_40A.jpeg) | ✅ | ✅ | +| **Random SSR** ![Random SSR](./assets/img/hardware/Random_SSR.jpeg) | ✅ | ✅ | +| **Zero-Cross SSR** (🚧) ![Zero-Cross SSR](./assets/img/hardware/SSR_40A_DA.jpeg) | ❌ | ✅ | +| **Voltage Regulator** (🚧) ![Loncont LSA-H3P50YB](./assets/img/hardware/LSA-H3P50YB.jpeg) | ✅ | ✅ | + +#### Bypass Relay (optional) + +Forces a heating at full power and bypass the dimmer at a given schedule or manually. +Keeping a dimmer `on` generates heat so a bypass relay can be installed to avoid using the dimmer. +If not installed, the dimmer will be used instead and will be set to 0-100% to simulate the relay. + +| Electromagnetic Relay | Zero-Cross SSR | Random SSR | +| :--------------------------------------------------------------: | :------------------------------------------------------: | :--------------------------------------------------: | +| ![Electromagnetic Relay](./assets/img/hardware/DIN_2_Relay.jpeg) | ![Zero-Cross SSR](./assets/img/hardware/SSR_40A_DA.jpeg) | ![Random SSR](./assets/img/hardware/Random_SSR.jpeg) | + +#### Temperature Sensor (optional) + +To monitor the temperature of the water tanker and trigger automatic heating based on temperature thresholds. Supported sensor: + +| DS18B20 | +| :--------------------------------------------: | +| ![DS18B20](./assets/img/hardware/DS18B20.jpeg) | + +#### Measurement device (optional) + +| PZEM-004T V3 | JSY-MK-194T | +| :---------------------------------------------------: | :-------------------------------------------------------------------------------------------: | +| can precisely monitor each output independently | can monitor the global routed power (sum of the two outputs) and grid power with its 2 clamps | +| ![PZEM-004T V3](./assets/img/hardware/PZEM-004T.jpeg) | ![JSY-MK-194T](./assets/img/hardware/JSY-MK-194T_2.jpeg) | + +#### Additional Features + +Each output supports the following features: + +- `Automatic Bypass` / `Manual Bypass Control`: Automatically force a heating as needed based on days, hours, temperature range, or control it manually +- `Automatic Dimmer` / `Manual Dimmer Control`: Automatically send the grid excess to the resistive load through the dimmer (or manually control the dimmer yourself if disabled), or control it manually +- `Dimmer Duty Limiter`: Set a limit to the dimmer power to avoid routing too much power +- `Dimmer Temperature Limiter`: Set a limit to the dimmer to stop it when a temperature is reached. This temperature can be different than the temperature used in auto bypass mode. +- `Statistics`: Harmonic information, power factor, energy, routed power, etc +- `Independent or Sequential Outputs with Grid Excess Sharing`: Outputs are sequential by default (second output activated after first one at 100%). + **Independent outputs are also supported** thanks to the sharing feature to split the excess between outputs. + +### Grid Power Measurement + +Measuring the grid power is essential to know how much power is available to route. +One of the following method is required, either with a JSY-MK-194T or remotely. + +#### JSY-MK-194T + +Preferred method to measure the grid power and routed power. it is accurate and reliable and store energy data. +_Note that the `JSY-MK-194T` has 2 channels, so it can be used both to measure the grid power but also to measure the total routed power of the router (2 outputs combined)._ +_It cannot be used though to independently measure each router output._ + +#### Remote Grid Measurement + +- `Remote JSY`: Read the grid power from a remote JSY at a rate of **20 messages per second** (nearly as fast as having the JSY wired to the ESP). There is no impact on routing precision! +- `MQTT`: Read the excess power remotely from a remote MQTT topic, for example from a **Shelly EM**, **Home Assistant**, **Jeedom**, etc. + This is less accurate but still works and facilitate the building of a router. + +#### 3-Phase Support + +The router outputs are mono-phase, but the router supports measuring power on a 3-phase systems: + +- Either by receiving the voltage and aggregated power through `MQTT` from a `Shelly 3EM` for example +- Either by using a `JSY-MK-333` (🚧) + +### 2x Relays + +- `NO / NC` relay type +- `Automatic Control` / `Manual Control`: You can specify the resistive load power in watts connected to the relay. + If you do so, the relay will be activated automatically based on the excess power. + +### Monitoring and Management + +- `Charts`: Live charts displayed for some important metrics +- `Display` Add I2C OLED Display (`SSD1307`, `SH1106`, `SH1107`) +- `Health Status`: configuration mistakes are detected as much as possible and issues displayed when a component was found to not properly work. +- `Languages (i18n)`: en / fr +- `LEDs`: Add LEDs for visual alerts +- `Manual Override`: Override anything remotely (MQTT, REST, Website) +- `Pinout Map`: 2 pinout maps show the view of configured pins and activated pins, to report issues, duplications or invalid pins. +- `Push Button`: Add a push button to restart the device +- `Restart`, `Factory Reset`, `Config Backup & Restore`, `Debug Logging` +- `Statistics`: Harmonic information, power factor, energy, routed power, grid power, grid frequency and voltage, etc +- `Temperature Sensor`: for router box heat monitoring (Supported type: `DS18B20`) +- `Web console`: View ESP logs live from a Web interface +- `Web OTA Firmware Update`: Update firmware through the Web interface + +### MQTT, REST API and Home Automation Systems + +The router exposes a lot of statistics and information through MQTT and REST API and provides a very good integration with Home Assistant or other home automation systems. +The router can be completely controlled remotely through a Home Automation System. + +- `REST API`: extensive REST API support +- `MQTT`: extensive MQTT API (with `TLS` support) +- [Home Assistant Integration](https://www.home-assistant.io/integrations/mqtt/#mqtt-discovery): Activate Home Assistant Auto Discovery to automatically create a YaS☀️lR device in Home Assistant with all the sensors + +### Networking / Offline + +- `Access Point Mode`: router can **work in AP mode without WiFi and Internet** +- `Admin Password`: to protect the website, API and Access Point +- `Captive Portal` a captive portal is started first time to help you connect the router +- `DNS & mDNS`: Discover the device on the network through mDNS (Bonjour) or DNS +- `Ethernet & Wifi`: **ESP32 boards with Ethernet and WiFi are supported** (see [Compatible ESP32 Boards](/build#compatible-esp32-boards)) +- `NTP` support to synchronize time and date with Internet. If not activated, it is still possible to manually sync with your browser. +- `Offline Mode`: **The router can work without WiFi, even teh features requiring time and date.** + +### PID Control and Tuning + +The router uses a PID controller to control the dimmers and you have full control over the PID parameters to tune it. +Demo: + +[![PID Tuning in YaSolR (Yet Another Solar Router)](http://img.youtube.com/vi/ygSpUxKYlUE/0.jpg)](http://www.youtube.com/watch?v=ygSpUxKYlUE "PID Tuning in YaSolR (Yet Another Solar Router)") + +### Remote Capabilities + +You can split the router in several modules to facilitate the installation. +Modules can communicate through `UDP` very fast, or through `ESP-Now` when WiFi is not available for a long-range communication. + +- **Setup 1**: measurement module on the main electric panel with a JSY, YaSolR router close to the loads: + + - `Remote JSY-MK-194T with UDP on same WiFi` (✅) + - `Remote JSY-MK-194T with ESP-Now` (🚧) + - `Remote JSY-MK-333 with UDP on same WiFi` (🚧) + - `Remote JSY-MK-333 with ESP-Now` (🚧) + +- **Setup 2**: YaSolR router with the main electric panel with measurement module, remote dimmers and relays close to the loads. + - `Remote Relays with UDP on same WiFi` (🚧) + - `Remote Relays with ESP-Now` (🚧) + - `Remote Dimmers with UDP on same WiFi` (🚧) + - `Remote Dimmers with ESP-Now` (🚧) + +### Virtual Excess and EV Charger Compatibility + +Thanks to power measurement, the router also provides these features: + +- `Virtual Excess`: Expose the virtual excess (MQTT, REST API, web) which is composed of the current excess plus the routing power +- `EV Charger Compatibility` (i.e OpenEVSE): Don't prevent an EV charge to start (router can have lower priority than an EV box to consume available production excess) + +### OSS vs PRO + +OSS and Pro firmware are the same, except that the PRO version relies on commercial (paid) libraries and provides some additional features based on a better dashboard. + +**The Pro version is only 25 euros** and gives access to all the perks of the Pro version below: + +| Feature | OSS (Free) | PRO (Paid) | +| -------------------------- | :--------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +| Dashboard | Overview **only** | Full Dashboard as seen in the screenshots | +| Manual Dimmer Control | Home Assistant
MQTT API
REST API | **From Dashboard**
Home Assistant
MQTT API
REST API | +| Manual Bypass Control | Home Assistant
MQTT API
REST API | **From Dashboard**
Home Assistant
MQTT API
REST API | +| Manual Relay Control | Home Assistant
MQTT API
REST API | **From Dashboard**
Home Assistant
MQTT API
REST API | +| Configuration | Debug Config Page | **From Dashboard**
Debug Config Page | +| Health View from Dashboard | ❌ | ✅ | +| Statistics and Charts | ❌ | ✅ | +| PZEM Pairing | ❌ | ✅ | +| Help & Support | [Facebook Group](https://www.facebook.com/groups/yasolr) | [Facebook Group](https://www.facebook.com/groups/yasolr)
[Forum](https://github.com/mathieucarbou/YaSolR-OSS/discussions)
[Bug Report](https://github.com/mathieucarbou/YaSolR-OSS/issues) | +| Web Console | [WebSerial Lite](https://github.com/mathieucarbou/WebSerialLite) | [WebSerial Pro](https://www.webserial.pro) | +| Dashboard | [ESP-DASH](https://github.com/ayushsharma82/ESP-DASH) | [ESP-DASH Pro](https://espdash.pro) | +| OTA Firmware Update | [ElegantOTA](https://github.com/ayushsharma82/ElegantOTA) | [ElegantOTA Pro](https://elegantota.pro) | + +The money helps funding the hardware necessary to test and develop the firmware. + +# Alternatives and Inspirations + +This project was inspired by the following awesome Solar Router projects: + +- [Routeur Solaire Apper](https://ota.apper-solaire.org) (Cyril P., _[xlyric](https://github.com/xlyric)_) +- [Routeur Solaire offgrid Réseautonome](https://github.com/SeByDocKy/routeur_solaire) (Sébastien P., _[SeByDocKy](https://github.com/SeByDocKy)_) +- [Routeur Solaire MK2 PV Router](https://www.mk2pvrouter.co.uk) (Robin Emley) +- [Routeur Solaire Mk2 PV Router](https://github.com/FredM67/Mk2PVRouter) (Frédéric M.) +- [Routeur Solaire Tignous](https://forum-photovoltaique.fr/viewtopic.php?f=110&t=40512) (Tignous) +- [Routeur Solaire MaxPV](https://github.com/Jetblack31/MaxPV) (Jetblack31) +- [Routeur solaire "Le Profes'Solaire"](https://sites.google.com/view/le-professolaire/routeur-professolaire) (Anthony G., _Le Profes'Solaire_) +- [Routeur solaire "Le Profes'Solaire"](https://github.com/benoit-cty/solar-router) (Adapation from Benoit C.) +- [Routeur solaire Multi-Sources Multi-Modes et Modulaire](https://f1atb.fr/fr/realisation-dun-routeur-photovoltaique-multi-sources-multi-modes-et-modulaire/) (André B., _[F1ATB](https://github.com/F1ATB)_) +- [Routeur solaire ESP Home](https://domo.rem81.com/index.php/2023/07/18/pv-routeur-solaire/) (Remy) diff --git a/docs/learn/APRIMA_-_CEM_2014-03-27.pdf b/docs/learn/APRIMA_-_CEM_2014-03-27.pdf new file mode 100644 index 0000000..38287e2 Binary files /dev/null and b/docs/learn/APRIMA_-_CEM_2014-03-27.pdf differ diff --git a/docs/learn/BELHADJ KHEIRA ET BOUZIR NESSRINE.pdf b/docs/learn/BELHADJ KHEIRA ET BOUZIR NESSRINE.pdf new file mode 100644 index 0000000..8aceaf2 Binary files /dev/null and b/docs/learn/BELHADJ KHEIRA ET BOUZIR NESSRINE.pdf differ diff --git a/docs/learn/CEI 61000-3-2.pdf b/docs/learn/CEI 61000-3-2.pdf new file mode 100644 index 0000000..b3f135b Binary files /dev/null and b/docs/learn/CEI 61000-3-2.pdf differ diff --git a/docs/learn/DESIGN OF SNUBBERS FOR POWER CIRCUITS.pdf b/docs/learn/DESIGN OF SNUBBERS FOR POWER CIRCUITS.pdf new file mode 100644 index 0000000..2019573 Binary files /dev/null and b/docs/learn/DESIGN OF SNUBBERS FOR POWER CIRCUITS.pdf differ diff --git a/docs/learn/Diverting surplus PV Power, by Robin Emley.pdf b/docs/learn/Diverting surplus PV Power, by Robin Emley.pdf new file mode 100644 index 0000000..68edc1b Binary files /dev/null and b/docs/learn/Diverting surplus PV Power, by Robin Emley.pdf differ diff --git a/docs/learn/HARMONICS CAUSES, EFFECTS AND MINIMIZATION.pdf b/docs/learn/HARMONICS CAUSES, EFFECTS AND MINIMIZATION.pdf new file mode 100644 index 0000000..e0ef9b2 Binary files /dev/null and b/docs/learn/HARMONICS CAUSES, EFFECTS AND MINIMIZATION.pdf differ diff --git "a/docs/learn/Impact de la pollution harmonique sur les mat\303\251riels de r\303\251seau.pdf" "b/docs/learn/Impact de la pollution harmonique sur les mat\303\251riels de r\303\251seau.pdf" new file mode 100644 index 0000000..3dd85c9 Binary files /dev/null and "b/docs/learn/Impact de la pollution harmonique sur les mat\303\251riels de r\303\251seau.pdf" differ diff --git a/docs/learn/NEW TRIACS - IS THE SNUBBER CIRCUIT NECESSARY.pdf b/docs/learn/NEW TRIACS - IS THE SNUBBER CIRCUIT NECESSARY.pdf new file mode 100644 index 0000000..94dc65e Binary files /dev/null and b/docs/learn/NEW TRIACS - IS THE SNUBBER CIRCUIT NECESSARY.pdf differ diff --git a/docs/learn/Optimized Random Integral Wave AC Control Algorithm.pdf b/docs/learn/Optimized Random Integral Wave AC Control Algorithm.pdf new file mode 100644 index 0000000..86a6dc4 Binary files /dev/null and b/docs/learn/Optimized Random Integral Wave AC Control Algorithm.pdf differ diff --git a/docs/learn/RC Snubber for TRIAC.pdf b/docs/learn/RC Snubber for TRIAC.pdf new file mode 100644 index 0000000..f90c94e Binary files /dev/null and b/docs/learn/RC Snubber for TRIAC.pdf differ diff --git a/docs/learn/Relais SSR.txt b/docs/learn/Relais SSR.txt new file mode 100644 index 0000000..c49e6a4 --- /dev/null +++ b/docs/learn/Relais SSR.txt @@ -0,0 +1,2 @@ +C'est le type de MOC qui définit le Random ou pas. Les 3021, 3023 (400 Volt), 3053 (600 Volt), 3052, 3073 (800V) sont "non zéro crossing". +Donc changement obligatoire s'ils sont équipés d'un MOC 3083, 3063, ... qui sont "zéro crossing" \ No newline at end of file diff --git a/docs/learn/Simulation/Simulation.png b/docs/learn/Simulation/Simulation.png new file mode 100644 index 0000000..a777940 Binary files /dev/null and b/docs/learn/Simulation/Simulation.png differ diff --git a/docs/learn/Simulation/TRIAC_ST_AKG.asy b/docs/learn/Simulation/TRIAC_ST_AKG.asy new file mode 100644 index 0000000..8f9fe39 --- /dev/null +++ b/docs/learn/Simulation/TRIAC_ST_AKG.asy @@ -0,0 +1,28 @@ +Version 4 +SymbolType CELL +LINE Normal 0 44 36 44 +LINE Normal 0 20 36 20 +LINE Normal 36 20 20 44 +LINE Normal 4 20 20 44 +LINE Normal 32 0 32 20 +LINE Normal 32 44 32 64 +LINE Normal 28 44 64 44 +LINE Normal 28 44 44 20 +LINE Normal 44 20 60 44 +LINE Normal 36 20 64 20 +LINE Normal 0 64 -16 64 +LINE Normal 0 64 20 44 +WINDOW 0 48 0 Left 2 +WINDOW 3 48 72 Left 2 +SYMATTR Value TRIAC_ST_AKG +SYMATTR Prefix X +SYMATTR Description Generic TRIAC symbol for use with a model that you supply. +PIN 32 0 NONE 0 +PINATTR PinName A +PINATTR SpiceOrder 1 +PIN -16 64 NONE 0 +PINATTR PinName G +PINATTR SpiceOrder 3 +PIN 32 64 NONE 0 +PINATTR PinName K +PINATTR SpiceOrder 2 diff --git a/docs/learn/Simulation/infos.txt b/docs/learn/Simulation/infos.txt new file mode 100644 index 0000000..ef7757d --- /dev/null +++ b/docs/learn/Simulation/infos.txt @@ -0,0 +1,27 @@ +Avatar du membremichelg34 +Messages : 769 +Enregistré le : 29 nov. 2021 14:42 +Departement/Region : 34 +Professionnel PV : Non +Contact : +Re: Router via TRIAC et "Pollution" du réseau +par michelg34 » 01 oct. 2023 10:27 + +Si vous voulez expérimenter par vous-même, il vaut mieux commencer par faire des simulations, c'est moins coûteux et moins dangereux... ;) + +Voilà un exemple avec LTspice (simulateur SPICE gratuit dispo ici https://www.analog.com/en/design-center/design-tools-and-calculators/ltspice-simulator.html) + + triac_LTspice_view_fft.zip +(12.11 Kio) Téléchargé 2 fois + + +Extraire les fichiers, ouvrir avec LTSpice le schéma (fichier .asc), lancer la simulation (Simulate->Run), activer la fenêtre de resultat (.wav) en cliquant dessus, puis faire une FFT (View->FFT, puis choisir I(L3)). Cela devrait afficher la pollution harmonique. + +image_2023-10-01_102316984.png + + +La simulation est exécutée 3x en variant une inductance L2 mise en sortie du triac, entre rien (1uH pour le fil) et des valeurs déjà élevées. +Après y-a plus qu'à bricoler/simuler avec des idées de génie pour tenter d'améliorer les choses... +Bon courage. +:sun: +6x Trina Solar Vertex S 395W + 3x APS DS3-L, toiture 17deg sud-ouest, latitude 43.6deg nord \ No newline at end of file diff --git a/docs/learn/Simulation/st_standard-snubberless_triacs_read-me.pdf b/docs/learn/Simulation/st_standard-snubberless_triacs_read-me.pdf new file mode 100644 index 0000000..266b043 Binary files /dev/null and b/docs/learn/Simulation/st_standard-snubberless_triacs_read-me.pdf differ diff --git a/docs/learn/Simulation/st_standard_snubberless_triacs.lib b/docs/learn/Simulation/st_standard_snubberless_triacs.lib new file mode 100644 index 0000000..fdf6e4f --- /dev/null +++ b/docs/learn/Simulation/st_standard_snubberless_triacs.lib @@ -0,0 +1,1768 @@ +* File : standard_snubberless_triacs.lib +* Revision : 10.0 +* Date : 21/01/2021 +* +********************************************************************** +* Please Read Carefully: +*Information in this document is provided solely in connection with ST products. STMicroelectronics NV and its subsidiaries (ST) reserve the +*right to make changes, corrections, modifications or improvements, to this document, and the products and services described herein at any +*time, without notice. +*All ST products are sold pursuant to STs terms and conditions of sale. +*Purchasers are solely responsible for the choice, selection and use of the ST products and services described herein, and ST assumes no +*liability whatsoever relating to the choice, selection or use of the ST products and services described herein. +*No license, express or implied, by estoppel or otherwise, to any intellectual property rights is granted under this document. If any part of this +*document refers to any third party products or services it shall not be deemed a license grant by ST for the use of such third party products +*or services, or any intellectual property contained therein or considered as a warranty covering the use in any manner whatsoever of such +*third party products or services or any intellectual property contained therein. +*UNLESS OTHERWISE SET FORTH IN STS TERMS AND CONDITIONS OF SALE ST DISCLAIMS ANY EXPRESS OR IMPLIED +*WARRANTY WITH RESPECT TO THE USE AND/OR SALE OF ST PRODUCTS INCLUDING WITHOUT LIMITATION IMPLIED +*WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE (AND THEIR EQUIVALENTS UNDER THE LAWS +*OF ANY JURISDICTION), OR INFRINGEMENT OF ANY PATENT, COPYRIGHT OR OTHER INTELLECTUAL PROPERTY RIGHT. +*UNLESS EXPRESSLY APPROVED IN WRITING BY AN AUTHORIZED ST REPRESENTATIVE, ST PRODUCTS ARE NOT +*RECOMMENDED, AUTHORIZED OR WARRANTED FOR USE IN MILITARY, AIR CRAFT, SPACE, LIFE SAVING, OR LIFE SUSTAINING +*APPLICATIONS, NOR IN PRODUCTS OR SYSTEMS WHERE FAILURE OR MALFUNCTION MAY RESULT IN PERSONAL INJURY, +*DEATH, OR SEVERE PROPERTY OR ENVIRONMENTAL DAMAGE. ST PRODUCTS WHICH ARE NOT SPECIFIED AS "AUTOMOTIVE +*GRADE" MAY ONLY BE USED IN AUTOMOTIVE APPLICATIONS AT USERS OWN RISK. +*Resale of ST products with provisions different from the statements and/or technical features set forth in this document shall immediately void +*any warranty granted by ST for the ST product or service described herein and shall not create or extend in any manner whatsoever, any +*liability of ST. +*ST and the ST logo are trademarks or registered trademarks of ST in various countries. +*Information in this document supersedes and replaces all information previously supplied. +*The ST logo is a registered trademark of STMicroelectronics. All other names are the property of their respective owners. +* 2008 STMicroelectronics - All rights reserved +*STMicroelectronics group of companies +*Australia - Belgium - Brazil - Canada - China - Czech Republic - Finland - France - Germany - Hong Kong - India - Israel - Italy - Japan - +*Malaysia - Malta - Morocco - Singapore - Spain - Sweden - Switzerland - United Kingdom - United States of America +*www.st.com +********************************************************************** +* +*************************************************************************** +* TRIACs PSpice Models * +*************************************************************************** +* Note : +* +* This TRIAC model simulates: +* -Igt (the same for all quadrants) MAX of the specification +*note: for 4 quadrants TRIAC, IGT Q4 is taken into account for all quadrants +* -Il (the same for all quadrants) Typ of the specification +* -Ih (the same for both polarity) Typ of the specification +* -VDRM +* -VRRM +* -(dI/dt)c and (dV/dt)c parameters are simulated only if those +* constraints exceed very highly the specified limits. +* -Power dissipation is realistic and correspond to a typical TRIAC +* +* All these parameters are constant, and don't vary neither with +* temperature nor other parameters. +* +* The "STANDARD" parameter switch between 4 quadrants TRIACs (STANDARD = 1) +* and 3 quadrants TRIACs (STANDARD = 0). +* The "STANDARD" parameter maintains or suppress the triggering possibility of +* the TRIAC in the fourth quadrant, and has absolutely NO EFFECT on other +* parameters. +* +* For a correct triac behavior, the "Maximum step size" must be below +* or equal 20s. +* +* +* +*$ +.subckt Triac_ST A K G PARAMS: ++ Vdrm=400v ++ Igt=20ma ++ Ih=6ma ++ Rt=0.01 ++ Standard=1 +* +* Vdrm : Repetitive forward off-state voltage +* Ih : Holding current +* Igt : Gate trigger current +* Rt : Dynamic on-state resistance +* Standard : Differenciation between Snubberless and Standard TRIACs +* (Standard=0 => Snubberless TRIACs, Standard=1 => Standard TRIACs) +* +**************************** +* Power circuit * +**************************** +* +**************************** +*Switch circuit* +**************************** +* Q1 & Q2 Conduction +S_S3 A Plip1 positive 0 Smain +*RS_S3 positive 0 1G +D_DAK1 Plip1 Plip2 Dak +R_Rlip Plip1 Plip2 1k +V_Viak Plip2 K DC 0 AC 0 +* +* Q3 & Q4 Conduction +S_S4 A Plin1 negative 0 Smain +*RS_S4 negative 0 1G +D_DKA1 Plin2 Plin1 Dak +R_Rlin Plin1 Plin2 1k +V_Vika K Plin2 DC 0 AC 0 +**************************** +*Gate circuit* +**************************** +R_Rgk G K 10G +D_DGKi Pg2 G Dgk +D_DGKd G Pg2 Dgk +V_Vig Pg2 K DC 0 AC 0 +R_Rlig G Pg2 1k +* +**************************** +*Interface circuit* +**************************** +* positive pilot +R_Rp Controlp positive 2.2 +C_Cp 0 positive 1u +E_IF15OR3 Controlp 0 VALUE {IF( ( (V(CMDIG)>0.5) | (V(CMDILIH)>0.5) | (V(CMDVdrm)>0.5) ),400,0 )} +* +* negative pilot +R_Rn Controln negative 2.2 +C_Cn 0 negative 1u +E_IF14OR3 Controln 0 VALUE {IF( ( (V(CMDIG)>0.5) | (V(CMDILIHN)>0.5) | (V(CMDVdrm)>0.5) ),400,0 )} +* +**************************** +* Pilots circuit * +**************************** +**************************** +* Pilot Gate * +**************************** +E_IF1IG inIG 0 VALUE {IF( ( ABS(I(V_Vig)) ) > (Igt-1u) ,1,0 )} +E_MULT2MULT CMDIG 0 VALUE {V(Q4)*V(inIG)} +E_IF2Quadrant4 Q4 0 VALUE {IF(((I(V_Vig)>(Igt-0.000001))&((V(A)-V(K))<0)&(Standard==0)),0,1)} +* +**************************** +* Pilot IHIL * +**************************** +* +E_IF10IL inIL 0 VALUE {IF( ((I(V_Viak))>(Ih/2)),1,0 )} +E_IF5IH inIH 0 VALUE {IF( ((I(V_Viak))>(Ih/3)),1,0 )} +* +* Flip_flop IHIL +E_IF6DIHIL SDIHIL 0 VALUE {IF((V(inIL)*V(inIH)+V(inIH)*(1-V(inIL))*(V(CMDILIH)) )>0.5,1,0)} +C_CIHIL CMDILIH 0 1n +R_RIHIL SDIHIL CMDILIH 1K +R_RIHIL2 CMDILIH 0 100Meg +* +**************************** +* Pilot IHILN * +**************************** +* +E_IF11ILn inILn 0 VALUE {IF( ((I(V_Vika))>(Ih/2)),1,0 )} +E_IF3IHn inIHn 0 VALUE {IF( ((I(V_Vika))>(Ih/3)),1,0 )} +* Flip_flop IHILn +E_IF4DIHILN SDIHILN 0 VALUE {IF((V(inILn)*V(inIHn)+V(inIHn)*(1-V(inILn))*(V(CMDILIHN)) )>0.5,1,0)} +C_CIHILn CMDILIHN 0 1n +R_RIHILn SDIHILN CMDILIHN 1K +R_RIHILn2 CMDILIHN 0 100Meg +* +**************************** +* Pilot VDRM * +**************************** +E_IF8Vdrm inVdrm 0 VALUE {IF( (ABS(V(A)-V(K))>(Vdrm*1.3)),1,0 )} +E_IF9IHVDRM inIhVdrm 0 VALUE {IF( (I(V_Viak)>(Vdrm*1.3)/1.2meg)| (I(V_Vika)>(Vdrm*1.3)/1.2meg),1,0)} +* Flip_flop VDRM +E_IF7DVDRM SDVDRM 0 VALUE {IF((V(inVdrm)+(1-V(inVdrm))*V(inIhVdrm)*V(CMDVdrm) )>0.5,1,0)} +C_CVdrm CMDVdrm 0 1n +R_RVdrm SDVDRM CMDVdrm 100 +R_RVdrm2 CMDVdrm 0 100Meg +* +**************************** +* Switch Model * +**************************** +.MODEL Smain VSWITCH Roff=1.2meg Ron={Rt} Voff=0 Von=100 +**************** +* Diodes Model * +**************** +.MODEL Dak D( Is=3E-12 Cjo=5pf) +.MODEL Dgk D( Is=1E-16 Cjo=50pf Rs=5) +.ends +* +********************************************************************* +* TRIACs PSpice Library * +********************************************************************* +********************************************************************* +* Standard TRIACs definition * +********************************************************************* +* +*$ +.subckt T4050-6 A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=50ma ++ Ih=85ma ++ Rt=0.010 ++ Standard=1 +* 2021 / ST / Rev 0 +.ends +*$ +.subckt T405-700 A K G +X1 A K G Triac_ST params: ++ Vdrm=700v ++ Igt=5ma ++ Ih=10ma ++ Rt=0.120 ++ Standard=1 +* 2021 / ST / Rev 0 +.ends +*$ +.subckt T435-700 A K G +X1 A K G Triac_ST params: ++ Vdrm=700v ++ Igt=35ma ++ Ih=35ma ++ Rt=0.120 ++ Standard=1 +* 2021 / ST / Rev 0 +.ends +*$ +.subckt T835-8G A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=35ma ++ Ih=35ma ++ Rt=0.05 ++ Standard=1 +* 2021 / ST / Rev 0 +.ends +*$ +.subckt T850-8G A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=50ma ++ Ih=75ma ++ Rt=0.05 ++ Standard=1 +* 2021 / ST / Rev 0 +.ends +*$ +.subckt T850-6G A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=50ma ++ Ih=75ma ++ Rt=0.05 ++ Standard=1 +* 2021 / ST / Rev 0 +.ends +*$ +.subckt T1210-6G A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=10ma ++ Ih=15ma ++ Rt=0.035 ++ Standard=1 +* 2021 / ST / Rev 0 +.ends +*$ +.subckt T1050-8G A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=50ma ++ Ih=50ma ++ Rt=0.040 ++ Standard=1 +* 2021 / ST / Rev 0 +.ends +*$ +.subckt T1035-6G A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=35ma ++ Ih=35ma ++ Rt=0.040 ++ Standard=1 +* 2021 / ST / Rev 0 +.ends +*$ +.subckt T830-8FP A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=30ma ++ Ih=50ma ++ Rt=0.04 ++ Standard=1 +* 2015 / ST / Rev 0 +.ends +*$ +.subckt Z00607MA A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=7ma ++ Ih=5ma ++ Rt=0.42 ++ Standard=1 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt Z0103M A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=5ma ++ Ih=7ma ++ Rt=0.4 ++ Standard=1 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt Z0103N A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=5ma ++ Ih=7ma ++ Rt=0.4 ++ Standard=1 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt Z0107M A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=7ma ++ Ih=10ma ++ Rt=0.4 ++ Standard=1 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt Z0107N A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=7ma ++ Ih=10ma ++ Rt=0.4 ++ Standard=1 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt Z0109M A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=10ma ++ Ih=10ma ++ Rt=0.4 ++ Standard=1 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt Z0109N A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=10ma ++ Ih=10ma ++ Rt=0.4 ++ Standard=1 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt Z0110M A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=25ma ++ Ih=25ma ++ Rt=0.4 ++ Standard=1 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt Z0402M A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=3ma ++ Ih=3ma ++ Rt=0.18 ++ Standard=1 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt Z0405M A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=5ma ++ Ih=5ma ++ Rt=0.18 ++ Standard=1 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt Z0405N A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=5ma ++ Ih=5ma ++ Rt=0.18 ++ Standard=1 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt Z0409M A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=10ma ++ Ih=10ma ++ Rt=0.18 ++ Standard=1 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt Z0409N A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=10ma ++ Ih=10ma ++ Rt=0.18 ++ Standard=1 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt Z0410M A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=25ma ++ Ih=25ma ++ Rt=0.18 ++ Standard=1 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt Z0410N A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=25ma ++ Ih=25ma ++ Rt=0.18 ++ Standard=1 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt BTB04-600SL A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=25ma ++ Ih=15ma ++ Rt=0.1 ++ Standard=1 +* 2008 / ST / Rev 0 +.ends +*$ +.subckt T405Q-600 A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=10ma ++ Ih=10ma ++ Rt=0.1 ++ Standard=1 +* 2008 / ST / Rev 0 +.ends +*$ +.subckt BTA06-600C A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=50ma ++ Ih=25ma ++ Rt=0.06 ++ Standard=1 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTA06-800C A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=50ma ++ Ih=25ma ++ Rt=0.06 ++ Standard=1 +* 2013 / ST / Rev 0 +.ends +*$ +.subckt BTB06-600C A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=50ma ++ Ih=25ma ++ Rt=0.06 ++ Standard=1 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTA06-600B A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=100ma ++ Ih=50ma ++ Rt=0.06 ++ Standard=1 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTB06-600B A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=100ma ++ Ih=50ma ++ Rt=0.06 ++ Standard=1 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTA08-600C A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=50ma ++ Ih=25ma ++ Rt=0.05 ++ Standard=1 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTA08-800C A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=50ma ++ Ih=25ma ++ Rt=0.05 ++ Standard=1 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTB08-600C A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=50ma ++ Ih=25ma ++ Rt=0.05 ++ Standard=1 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTB08-800C A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=50ma ++ Ih=25ma ++ Rt=0.05 ++ Standard=1 +* 2013 / ST / Rev 0 +.ends +*$ +.subckt BTA08-600B A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=100ma ++ Ih=50ma ++ Rt=0.05 ++ Standard=1 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTB08-600B A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=100ma ++ Ih=50ma ++ Rt=0.05 ++ Standard=1 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTA10-600C A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=50ma ++ Ih=25ma ++ Rt=0.04 ++ Standard=1 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt BTA10-600B A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=100ma ++ Ih=50ma ++ Rt=0.04 ++ Standard=1 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt BTA12-600C A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=50ma ++ Ih=25ma ++ Rt=0.035 ++ Standard=1 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTB12-600C A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=50ma ++ Ih=25ma ++ Rt=0.035 ++ Standard=1 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTA12-600B A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=100ma ++ Ih=50ma ++ Rt=0.035 ++ Standard=1 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTA12-800B A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=100ma ++ Ih=50ma ++ Rt=0.035 ++ Standard=1 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTB12-600B A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=100ma ++ Ih=50ma ++ Rt=0.035 ++ Standard=1 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTB12-800B A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=100ma ++ Ih=50ma ++ Rt=0.035 ++ Standard=1 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTA16-600B A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=100ma ++ Ih=50ma ++ Rt=0.025 ++ Standard=1 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt BTA16-600C A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=50ma ++ Ih=25ma ++ Rt=0.025 ++ Standard=1 +* 2008 / ST / Rev 0 +.ends +*$ +.subckt BTA16-800B A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=100ma ++ Ih=50ma ++ Rt=0.025 ++ Standard=1 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt BTB16-600B A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=100ma ++ Ih=50ma ++ Rt=0.025 ++ Standard=1 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt BTB16-600C A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=50ma ++ Ih=25ma ++ Rt=0.025 ++ Standard=1 +* 2008 / ST / Rev 0 +.ends +*$ +.subckt BTB16-800B A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=100ma ++ Ih=50ma ++ Rt=0.025 ++ Standard=1 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt BTB24-600B A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=100ma ++ Ih=80ma ++ Rt=0.016 ++ Standard=1 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTB24-800B A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=100ma ++ Ih=80ma ++ Rt=0.016 ++ Standard=1 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTA25-600B A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=100ma ++ Ih=80ma ++ Rt=0.016 ++ Standard=1 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt BTA25-800B A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=100ma ++ Ih=80ma ++ Rt=0.016 ++ Standard=1 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt BTA26-600B A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=100ma ++ Ih=80ma ++ Rt=0.016 ++ Standard=1 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTA26-800B A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=100ma ++ Ih=80ma ++ Rt=0.016 ++ Standard=1 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTB26-600B A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=100ma ++ Ih=80ma ++ Rt=0.016 ++ Standard=1 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTA40-600B A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=100ma ++ Ih=80ma ++ Rt=0.010 ++ Standard=1 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt BTA40-800B A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=100ma ++ Ih=80ma ++ Rt=0.010 ++ Standard=1 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt BTA41-600B A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=100ma ++ Ih=80ma ++ Rt=0.01 ++ Standard=1 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt BTA41-800B A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=100ma ++ Ih=80ma ++ Rt=0.01 ++ Standard=1 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt BTB41-600B A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=100ma ++ Ih=80ma ++ Rt=0.01 ++ Standard=1 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt BTB41-800B A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=100ma ++ Ih=80ma ++ Rt=0.01 ++ Standard=1 +* 1999 / ST / Rev 0 +.ends +********************************************************************* +* Snubberless & Logic Level Triac definition * +********************************************************************* +*$ +.subckt T2650-6PF A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=50ma ++ Ih=75ma ++ Rt=0.014 ++ Standard=0 +* 2021 / ST / Rev 0 +.ends +*$ +.subckt T405-600 A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=5ma ++ Ih=10ma ++ Rt=0.12 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt T405-800 A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=5ma ++ Ih=10ma ++ Rt=0.12 ++ Standard=0 +* 2008 / ST / Rev 0 +.ends +*$ +.subckt T410-600 A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=10ma ++ Ih=15ma ++ Rt=0.12 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt T410-800 A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=10ma ++ Ih=15ma ++ Rt=0.12 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt T435-600 A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=35ma ++ Ih=35ma ++ Rt=0.12 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt T435-800 A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=35ma ++ Ih=35ma ++ Rt=0.12 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTA06-600TW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=5ma ++ Ih=10ma ++ Rt=0.06 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTB06-600TW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=5ma ++ Ih=10ma ++ Rt=0.06 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTA06-600SW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=10ma ++ Ih=15ma ++ Rt=0.06 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTB06-600SW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=10ma ++ Ih=15ma ++ Rt=0.06 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTA06-600CW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=35ma ++ Ih=35ma ++ Rt=0.06 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTA06-800TW A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=5ma ++ Ih=10ma ++ Rt=0.06 ++ Standard=0 +* 2010 / ST / Rev 0 +.ends +*$ +.subckt BTB06-600CW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=35ma ++ Ih=35ma ++ Rt=0.06 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTA06-600BW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=50ma ++ Ih=50ma ++ Rt=0.06 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTA06-800BW A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=50ma ++ Ih=50ma ++ Rt=0.06 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTB06-600BW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=50ma ++ Ih=50ma ++ Rt=0.06 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt T630-800 A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=30ma ++ Ih=50ma ++ Rt=0.05 ++ Standard=0 +* 2010 / ST / Rev 0 +.ends +*$ +.subckt BTA08-600TW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=5ma ++ Ih=10ma ++ Rt=0.05 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTB08-600TW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=5ma ++ Ih=10ma ++ Rt=0.05 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTB08-800TW A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=5ma ++ Ih=10ma ++ Rt=0.05 ++ Standard=0 +* 2008 / ST / Rev 0 +.ends +*$ +.subckt BTA08-600SW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=10ma ++ Ih=15ma ++ Rt=0.05 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTB08-600SW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=10ma ++ Ih=15ma ++ Rt=0.05 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTA08-600CW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=35ma ++ Ih=35ma ++ Rt=0.056 ++ Standard=0 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt BTB08-600CW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=35ma ++ Ih=35ma ++ Rt=0.05 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTA08-600BW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=50ma ++ Ih=50ma ++ Rt=0.05 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTA08-800BW A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=50ma ++ Ih=50ma ++ Rt=0.05 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTB08-600BW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=50ma ++ Ih=50ma ++ Rt=0.05 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt T810-600 A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=10ma ++ Ih=15ma ++ Rt=0.05 ++ Standard=0 +* 2008 / ST / Rev 0 +.ends +*$ +.subckt T810-800 A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=10ma ++ Ih=15ma ++ Rt=0.05 ++ Standard=0 +* 2008 / ST / Rev 0 +.ends +*$ +.subckt T835-600 A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=35ma ++ Ih=35ma ++ Rt=0.05 ++ Standard=0 +* 2008 / ST / Rev 0 +.ends +*$ +.subckt T835-800 A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=35ma ++ Ih=35ma ++ Rt=0.05 ++ Standard=0 +* 2008 / ST / Rev 0 +.ends +*$ +.subckt BTA10-600CW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=35ma ++ Ih=35ma ++ Rt=0.04 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTA10-800CW A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=35ma ++ Ih=35ma ++ Rt=0.04 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTA10-600BW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=50ma ++ Ih=50ma ++ Rt=0.04 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTA10-800BW A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=50ma ++ Ih=50ma ++ Rt=0.04 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTB10-600BW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=50ma ++ Ih=50ma ++ Rt=0.04 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTB10-800BW A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=50ma ++ Ih=50ma ++ Rt=0.04 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTA12-600TW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=5ma ++ Ih=10ma ++ Rt=0.035 ++ Standard=0 +* 2008 / ST / Rev 0 +.ends +*$ +.subckt BTA12-600SW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=10ma ++ Ih=15ma ++ Rt=0.035 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTB12-600SW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=10ma ++ Ih=15ma ++ Rt=0.035 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTA12-600CW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=35ma ++ Ih=35ma ++ Rt=0.035 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTA12-800SW A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=10ma ++ Ih=15ma ++ Rt=0.035 ++ Standard=0 +* 2008 / ST / Rev 0 +.ends +*$ +.subckt BTA12-800CW A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=35ma ++ Ih=35ma ++ Rt=0.035 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTB12-600CW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=35ma ++ Ih=35ma ++ Rt=0.035 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTB12-800CW A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=35ma ++ Ih=35ma ++ Rt=0.035 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTA12-600BW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=50ma ++ Ih=50ma ++ Rt=0.035 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTA12-800BW A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=50ma ++ Ih=50ma ++ Rt=0.035 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTB12-600BW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=50ma ++ Ih=50ma ++ Rt=0.035 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt T1210-800 A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=10ma ++ Ih=15ma ++ Rt=0.035 ++ Standard=0 +* 2008 / ST / Rev 0 +.ends +*$ +.subckt T1235-600 A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=35ma ++ Ih=35ma ++ Rt=0.035 ++ Standard=0 +* 2008 / ST / Rev 0 +.ends +*$ +.subckt T1235-800 A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=35ma ++ Ih=35ma ++ Rt=0.035 ++ Standard=0 +* 2008 / ST / Rev 0 +.ends +*$ +.subckt BTA16-600CW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=35ma ++ Ih=35ma ++ Rt=0.025 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTA16-800CW A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=35ma ++ Ih=35ma ++ Rt=0.025 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTB16-600CW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=35ma ++ Ih=35ma ++ Rt=0.025 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTB16-800CW A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=35ma ++ Ih=35ma ++ Rt=0.025 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTA16-600BW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=50ma ++ Ih=50ma ++ Rt=0.025 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTA16-800BW A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=50ma ++ Ih=50ma ++ Rt=0.025 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTB16-600BW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=50ma ++ Ih=50ma ++ Rt=0.025 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTB16-800BW A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=50ma ++ Ih=50ma ++ Rt=0.025 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt BTA16-600SW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=10ma ++ Ih=15ma ++ Rt=0.025 ++ Standard=0 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt BTB16-600SW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=10ma ++ Ih=15ma ++ Rt=0.025 ++ Standard=0 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt BTB16-800SW A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=10ma ++ Ih=15ma ++ Rt=0.025 ++ Standard=0 +* 2008 / ST / Rev 0 +.ends +*$ +.subckt T1610-800 A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=10ma ++ Ih=15ma ++ Rt=0.025 ++ Standard=0 +* 2010 / ST / Rev 0 +.ends +*$ +.subckt T1610-600 A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=10ma ++ Ih=15ma ++ Rt=0.025 ++ Standard=0 +* 2010 / ST / Rev 0 +.ends +*$ +.subckt T1620-600W A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=20ma ++ Ih=35ma ++ Rt=0.02 ++ Standard=0 +* 2008 / ST / Rev 1 +.ends +*$ +.subckt T1635-600 A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=35ma ++ Ih=35ma ++ Rt=0.025 ++ Standard=0 +* 2008 / ST / Rev 0 +.ends +*$ +.subckt BTA20-600CW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=35ma ++ Ih=50ma ++ Rt=0.02 ++ Standard=0 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt BTA24-600CW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=35ma ++ Ih=50ma ++ Rt=0.016 ++ Standard=0 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt BTA24-800CW A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=35ma ++ Ih=50ma ++ Rt=0.016 ++ Standard=0 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt BTB24-600CW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=35ma ++ Ih=50ma ++ Rt=0.016 ++ Standard=0 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt BTB24-800CW A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=35ma ++ Ih=50ma ++ Rt=0.016 ++ Standard=0 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt BTA24-600BW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=50ma ++ Ih=75ma ++ Rt=0.016 ++ Standard=0 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt BTA24-800BW A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=50ma ++ Ih=75ma ++ Rt=0.016 ++ Standard=0 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt BTB24-600BW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=50ma ++ Ih=75ma ++ Rt=0.016 ++ Standard=0 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt BTB24-800BW A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=50ma ++ Ih=75ma ++ Rt=0.016 ++ Standard=0 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt BTA25-600BW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=50ma ++ Ih=75ma ++ Rt=0.016 ++ Standard=0 +* 2008 / ST / Rev 0 +.ends +*$ +.subckt BTA25-800BW A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=50ma ++ Ih=75ma ++ Rt=0.016 ++ Standard=0 +* 2008 / ST / Rev 0 +.ends +*$ +.subckt T2535-600 A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=35ma ++ Ih=50ma ++ Rt=0.016 ++ Standard=0 +* 2008 / ST / Rev 0 +.ends +*$ +.subckt T2535-800 A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=35ma ++ Ih=50ma ++ Rt=0.016 ++ Standard=0 +* 2008 / ST / Rev 0 +.ends +*$ +.subckt BTA26-800CW A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=35ma ++ Ih=50ma ++ Rt=0.016 ++ Standard=0 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt BTA26-600BW A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=50ma ++ Ih=75ma ++ Rt=0.016 ++ Standard=0 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt BTA26-800BW A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=50ma ++ Ih=75ma ++ Rt=0.016 ++ Standard=0 +* 1999 / ST / Rev 0 +.ends +*$ +.subckt T2550-12 A K G +X1 A K G Triac_ST params: ++ Vdrm=1200v ++ Igt=50ma ++ Ih=60ma ++ Rt=0.02 ++ Standard=0 +* 2014 / ST / Rev 0 +.ends +* +********************************************************************* +* Alternistors definition * +********************************************************************* +* +*$ +.subckt TXDV812 A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=100ma ++ Ih=100ma ++ Rt=0.04 ++ Standard=0 +* 2011 / ST / Rev 0 +.ends +*$ +.subckt TXDV1212 A K G +X1 A K G Triac_ST params: ++ Vdrm=1200v ++ Igt=100ma ++ Ih=100ma ++ Rt=0.04 ++ Standard=0 +* 2011 / ST / Rev 0 +.ends +*$ +.subckt TPDV640 A K G +X1 A K G Triac_ST params: ++ Vdrm=600v ++ Igt=200ma ++ Ih=50ma ++ Rt=0.012 ++ Standard=0 +* 2011 / ST / Rev 0 +.ends +*$ +.subckt TPDV840 A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=200ma ++ Ih=50ma ++ Rt=0.012 ++ Standard=0 +* 2011 / ST / Rev 0 +.ends +*$ +.subckt TPDV1240 A K G +X1 A K G Triac_ST params: ++ Vdrm=1200v ++ Igt=200ma ++ Ih=50ma ++ Rt=0.012 ++ Standard=0 +* 2011 / ST / Rev 0 +.ends +*$ +.subckt TPDV825 A K G +X1 A K G Triac_ST params: ++ Vdrm=800v ++ Igt=150ma ++ Ih=50ma ++ Rt=0.019 ++ Standard=0 +* 2011 / ST / Rev 0 +.ends +*$ +.subckt TPDV1025 A K G +X1 A K G Triac_ST params: ++ Vdrm=1000v ++ Igt=150ma ++ Ih=50ma ++ Rt=0.019 ++ Standard=0 +* 2011 / ST / Rev 0 +.ends +*$ +.subckt TPDV1225 A K G +X1 A K G Triac_ST params: ++ Vdrm=1200v ++ Igt=150ma ++ Ih=50ma ++ Rt=0.019 ++ Standard=0 +* 2011 / ST / Rev 0 +.ends +*$ diff --git a/docs/learn/Simulation/triac23_filter.asc b/docs/learn/Simulation/triac23_filter.asc new file mode 100644 index 0000000..a645101 --- /dev/null +++ b/docs/learn/Simulation/triac23_filter.asc @@ -0,0 +1,98 @@ +Version 4 +SHEET 1 1340 756 +WIRE -816 -288 -1120 -288 +WIRE -176 -288 -736 -288 +WIRE 256 -288 -176 -288 +WIRE 560 -288 256 -288 +WIRE 560 -160 560 -288 +WIRE 752 -160 560 -160 +WIRE 752 -128 752 -160 +WIRE 256 -112 256 -288 +WIRE -176 -80 -176 -288 +WIRE 512 -80 400 -80 +WIRE 560 -80 560 -160 +WIRE 400 -48 400 -80 +WIRE 752 -16 752 -48 +WIRE 400 64 400 32 +WIRE 560 80 560 -16 +WIRE 752 80 752 48 +WIRE 752 80 560 80 +WIRE 48 112 0 112 +WIRE 112 112 48 112 +WIRE 256 112 256 -32 +WIRE 256 112 192 112 +WIRE 336 112 256 112 +WIRE 0 144 0 112 +WIRE -1120 176 -1120 -288 +WIRE -176 256 -176 0 +WIRE 0 256 0 224 +WIRE 0 256 -176 256 +WIRE 400 256 400 160 +WIRE 400 256 0 256 +WIRE 560 352 560 80 +WIRE 656 352 560 352 +WIRE 1280 352 736 352 +WIRE 1280 464 1280 352 +WIRE -1120 672 -1120 256 +WIRE 1280 672 1280 544 +WIRE 1280 672 -1120 672 +WIRE -1120 720 -1120 672 +FLAG 48 112 cde +FLAG -1120 720 0 +SYMBOL voltage -176 -96 R0 +WINDOW 123 0 0 Left 0 +WINDOW 39 0 0 Left 0 +SYMATTR InstName V1 +SYMATTR Value 5 +SYMBOL voltage 0 128 R0 +WINDOW 3 -36 160 Left 2 +WINDOW 123 0 0 Left 0 +WINDOW 39 0 0 Left 0 +SYMATTR Value PULSE(5 0 5m 10n 10n 100u 10m) +SYMATTR InstName V2 +SYMBOL res 736 -144 R0 +SYMATTR InstName R1 +SYMATTR Value 100 +SYMBOL cap 736 -16 R0 +SYMATTR InstName C1 +SYMATTR Value 1n +SYMBOL res 384 -64 R0 +SYMATTR InstName R2 +SYMATTR Value 47 +SYMBOL pnp 336 160 M180 +SYMATTR InstName Q1 +SYMATTR Value BC327-25 +SYMBOL res 240 -128 R0 +SYMATTR InstName R4 +SYMATTR Value 10k +SYMBOL voltage -1120 160 R0 +WINDOW 123 0 0 Left 0 +WINDOW 39 0 0 Left 0 +SYMATTR InstName V3 +SYMATTR Value SINE(0 325 50) +SYMBOL res 1264 448 R0 +SYMATTR InstName R3 +SYMATTR Value 23 +SYMBOL TRIAC_ST_AKG 528 -16 M180 +SYMATTR InstName U1 +SYMATTR Value BTA26-800CW +SYMBOL res 208 96 R90 +WINDOW 0 0 56 VBottom 2 +WINDOW 3 32 56 VTop 2 +SYMATTR InstName R5 +SYMATTR Value 10 +SYMBOL ind 752 336 R90 +WINDOW 0 5 56 VBottom 2 +WINDOW 3 32 56 VTop 2 +SYMATTR InstName L2 +SYMATTR Value {L} +SYMBOL ind -832 -272 R270 +WINDOW 0 32 56 VTop 2 +WINDOW 3 5 56 VBottom 2 +SYMATTR InstName L3 +SYMATTR Value 50 +SYMATTR SpiceLine Rser=10m +TEXT 640 168 Left 2 !.inc st_standard_snubberless_triacs.lib +TEXT 144 344 Left 2 !.tran 1000m +TEXT -1104 -376 Left 2 ;cable BT R+jX , X=Lw=0.08ohm/km = 250uH/km (2x aller et retour) +TEXT 16 384 Left 2 !.step param L list 1u 1600u 4700u diff --git a/docs/learn/Simulation/triac23_filter.plt b/docs/learn/Simulation/triac23_filter.plt new file mode 100644 index 0000000..7348830 Binary files /dev/null and b/docs/learn/Simulation/triac23_filter.plt differ diff --git a/docs/learn/Solid State Relay Guide - Phidgets Support.pdf b/docs/learn/Solid State Relay Guide - Phidgets Support.pdf new file mode 100644 index 0000000..5c909f6 Binary files /dev/null and b/docs/learn/Solid State Relay Guide - Phidgets Support.pdf differ diff --git a/docs/learn/Solid State Relays.pdf b/docs/learn/Solid State Relays.pdf new file mode 100644 index 0000000..9b9ae45 Binary files /dev/null and b/docs/learn/Solid State Relays.pdf differ diff --git a/docs/learn/TRIAC.pdf b/docs/learn/TRIAC.pdf new file mode 100644 index 0000000..aa63445 Binary files /dev/null and b/docs/learn/TRIAC.pdf differ diff --git a/docs/learn/introduction_to_pid_controllers_ed2.pdf b/docs/learn/introduction_to_pid_controllers_ed2.pdf new file mode 100644 index 0000000..ee874c8 Binary files /dev/null and b/docs/learn/introduction_to_pid_controllers_ed2.pdf differ diff --git a/docs/learn/zdc.txt b/docs/learn/zdc.txt new file mode 100644 index 0000000..4d293bb --- /dev/null +++ b/docs/learn/zdc.txt @@ -0,0 +1,4 @@ +- https://www.bristolwatch.com/ele2/zcnew.htm +- https://www.electronics-lab.com/project/ac-voltage-zero-cross-detector/ +- https://dextrel.net/dextrel-start-page/design-ideas-2/mains-zero-crossing-detector +- https://www.pcbway.com/project/shareproject/Zero_Cross_Detector_a707a878.html diff --git a/docs/manual.md b/docs/manual.md new file mode 100644 index 0000000..2370e7c --- /dev/null +++ b/docs/manual.md @@ -0,0 +1,665 @@ +--- +layout: default +title: Manual +description: Manual +--- + +# YaS☀️lR Manual + +- [Quick Start](#quick-start) + - [Initial Firmware Installation](#initial-firmware-installation) + - [Captive Portal (Access Point) and WiFi](#captive-portal-access-point-and-wifi) + - [Access Point Mode](#access-point-mode) +- [YaS☀️lR Pages](#yas%E2%98%80%EF%B8%8Flr-pages) + - [`/config`](#config) + - [`/console`](#console) + - [`/update`](#update) +- [Dashboard / Overview](#dashboard--overview) +- [Dashboard / Output 1 & 2](#dashboard--output-1--2) +- [Dashboard / Relays](#dashboard--relays) +- [Dashboard / Management](#dashboard--management) +- [Dashboard / Network](#dashboard--network) +- [Dashboard / MQTT](#dashboard--mqtt) + - [MQTT as a Grid Source](#mqtt-as-a-grid-source) + - [Home Assistant Discovery](#home-assistant-discovery) +- [Dashboard / GPIO](#dashboard--gpio) +- [Dashboard / Hardware](#dashboard--hardware) +- [Dashboard / Hardware Config](#dashboard--hardware-config) + - [Resistance Calibration](#resistance-calibration) +- [Dashboard / PID Controller](#dashboard--pid-controller) +- [Dashboard / Statistics](#dashboard--statistics) +- [Important Hardware Information](#important-hardware-information) + - [Bypass Relay](#bypass-relay) + - [Display](#display) + - [JSY-MK-194T](#jsy-mk-194t) + - [Remote JSY](#remote-jsy) + - [LEDs](#leds) + - [PZEM-004T V3](#pzem-004t-v3) + - [Pairing procedure](#pairing-procedure) + - [Temperature Sensor](#temperature-sensor) + - [Zero-Cross Detection](#zero-cross-detection) + - [Compatibility with EV box like OpenEVSE](#compatibility-with-ev-box-like-openevse) +- [Help and support](#help-and-support) + +## Quick Start + +When everything is wired and installed properly, you can: + +1. Flash the downloaded firmware (see [Initial Firmware Installation](#initial-firmware-installation)) +2. Power on the system to start the application +3. Connect to the WiFI: `YaSolR-xxxxxx` +4. Connected to the Captive Portal to setup your WiFi (see: [Captive Portal (Access Point) and WiFi](#captive-portal-access-point-and-wifi)) +5. Go to the [Dashboard / GPIO](#dashboard--gpio) page to verify your GPIOs. +6. Go to the [Dashboard / Hardware](#dashboard--hardware) page to activate the hardware you have +7. Go to the [Dashboard / Hardware Config](#dashboard--hardware-config) page to configure your hardware settings and resistance values if needed. + [Resistance Calibration](#resistance-calibration) is really important to do otherwise the router will not work. +8. Go to the [Dashboard / MQTT](#dashboard--mqtt) page to configure your MQTT settings if needed. +9. Go to the [Dashboard / Relays](#dashboard--relays) page to configure your relay loads if needed. +10. Go to [Dashboard / Output 1 & 2](#dashboard--output-1--2) pages to configure your bypass options and dimmer settings if needed. +11. Restart to activate everything. +12. Enjoy your YaS☀️lR! + +### Initial Firmware Installation + +The firmware file which must be used for a first installation is the one ending with `.FACTORY.bin`: + +Firmware can be downloaded here : [![Download](https://img.shields.io/badge/Download-firmware-green.svg)](https://yasolr.carbou.me/download) + +Flash with `esptool.py` (Linux / MacOS): + +```bash +# Erase the memory (including the user data) +esptool.py \ + --port /dev/ttyUSB0 \ + erase_flash +``` + +```bash +# Flash initial firmware and partitions +esptool.py \ + --port /dev/ttyUSB0 \ + --chip esp32 \ + --before default_reset \ + --after hard_reset \ + write_flash \ + --flash_mode dout \ + --flash_freq 40m \ + --flash_size detect \ + 0x0 YaSolR-VERSION-MODEL-CHIP.FACTORY.bin +``` + +Do not forget to change the port `/dev/ttyUSB0` to the one matching your system. +For example on Mac, it is often `/dev/cu.usbserial-0001` instead of `/dev/ttyUSB0`. + +With [Espressif Flash Tool](https://www.espressif.com/en/support/download/other-tools) (Windows): + +**Be careful to not forget the `0`.** + +![Espressif Flash Tool](assets/img/screenshots/Espressif_Flash_Tool.png) + +### Captive Portal (Access Point) and WiFi + +> Captive Portal and Access Point address: [http://192.168.4.1/](http://192.168.4.1/) + +A captive portal (Access Point) is started for the first time to configure the WiFi network, or when the application starts and cannot join an already configured WiFi network fro 15 seconds. + +![](assets/img/screenshots/Captive_Portal.jpeg) + +The captive portal is only started for 3 minutes, to allow configuring a (new) WiFi network. +After this delay, the portal will close, and the application will try to connect again to the WiFi. +And again, if the WiFi cannot be reached, connected to, or is not configured, the portal will be started again. + +This behavior allows to still have access to the application in case of a WiFi network change, or after a power failure, when the application restarts. +If the application restarts before the WiFi is available, it will launch the portal for 3 minutes, then restart and try to join the network again. + +In case of WiFi disruption (WiFi temporary down), the application will keep trying to reconnect. +If it is restarted and the WiFi is still not available, the Captive Portal will be launched. + +### Access Point Mode + +You can also chose to not connect to your Home WiFi and keep the AP mode active. +In this case, you will need to connect to the router WiFi each time you want to access it. + +In AP mode, all the features depending on Internet access and time are not available (MQTT, NTP). +You will have to manually sync the time from your browser to activate the auto bypass feature. + +## YaS☀️lR Pages + +Here are the main links to know about in the application: + +- `http://yasolr.local/`: Dashboard +- `http://yasolr.local/console`: Web Console +- `http://yasolr.local/update`: Web OTA (firmware update) +- `http://yasolr.local/config`: Debug Configuration Page +- `http://yasolr.local/api`: [REST API](rest) + +_(replace `yasolr.local` with the IP address of the router)_ + +### `/config` + +This page is accessible at: `http:///config`. +It allows to see the raw current configuration of the router and edit it. +This page should not normally be used, except for debugging purposes. + +[![](assets/img/screenshots/config.jpeg)](assets/img/screenshots/config.jpeg) + +### `/console` + +A Web Console is accessible at: `http:///console`. +You can see more logs if you activate Debug logging (but it will make the router react a bit more slowly). + +[![](assets/img/screenshots/console.jpeg)](assets/img/screenshots/console.jpeg) + +### `/update` + +Go to the Web OTA at `http:///update` to update the firmware over the air: + +[![](assets/img/screenshots/update.jpeg)](assets/img/screenshots/update.jpeg) + +The firmware file which must be used is the one ending with `.UPDATE.bin`: + +`YaSolR---.UPDATE.bin` + +## Dashboard / Overview + +The overview section shows some global information about the router. + +[![](assets/img/screenshots/overview.jpeg)](assets/img/screenshots/overview.jpeg) + +The temperature is coming from the sensor installed in the router box. + +## Dashboard / Output 1 & 2 + +The output sections show the state of the outputs and the possibility to control them. + +| [![](assets/img/screenshots/output1.jpeg)](assets/img/screenshots/output1.jpeg) | [![](assets/img/screenshots/output2.jpeg)](assets/img/screenshots/output2.jpeg) | + +- `Status` + - `Disabled`: Output is disabled (dimmer disabled or other reason) + - `Idle`: Output is not routing and not in bypass mode + - `Routing`: Routing in progress + - `Bypass`: Bypass has been activated manually + - `Bypass Auto`: Bypass has been activated based on automatic rules +- `Temperature`: This is the temperature reported by the sensor in water tank + +**Energy:** + +- `Power`: Routed power. +- `Apparent Power`: Apparent power in VA circulating on the wires. +- `Power Factor`: Power factor (if lower than 1, mainly composed of harmonic component). Ideal is close to 1. +- `THDi`: This is the estimated level of harmonics generated by this output. The lower, the better. +- `Voltage`: The dimmed RMS voltage sent to the resistive load. +- `Current`: The current in Amp sent to the resistive load. +- `Resistance`: The resistance of the load. +- `Energy`: The total accumulated energy routed by this output, stored in hardware (JSY and/or PZEM). + +**Dimmer Control:** + +- `Dimmer Automatic Control`: ON/OFF switch to select automatic routing mode or manual control of the dimmer. + Resistance calibration step is required before using automatic mode. +- `Dimmer Level` / `Dimmer Level Manual Control`: Slider to control the dimmer level manually. Only available when the dimmer is not in automatic mode. Otherwise the dimmer level is displayed. +- `Grid Excess Reserved`: Only available in automatic mode. Allows to share the remaining grid excess to the second output. + For example, if output 1 is set to 50%, then output 1 will take at most 50% of the grid excess (eventually less if 50% of the grid excess exceeds the nominal power of the connected load). Output 2 will be dimmed with the remaining excess. +- `Dimmer Duty Limiter`: Slider to limit the level of the dimmer in order to limit the routed power. +- `Dimmer Temperature Limiter`: Temperature threshold when the dimmer will stop routing. This temperature can be different than the temperature used in auto bypass mode. + +**Bypass Control:** + +- `Bypass Automatic Control`: ON/OFF switch to select automatic bypass mode or manual control of the bypass. +- `Bypass` / `Bypass Manual Control`: Switch to control the bypass manually. Only available when the bypass is not in automatic mode. Otherwise the bypass state is displayed. +- `Bypass Week Days`: Days of the week when the bypass can be activated. +- `Bypass Start Time` / `Bypass Stop Time`: The time range when the auto bypass is allowed to start. +- `Bypass Start Temperature`: The temperature threshold when the auto bypass will start: the temperature of the water tank needs to be lower than this threshold. +- `Bypass Stop Temperature`: The temperature threshold when the auto bypass will stop: the temperature of the water tank needs to be higher than this threshold. + +**All these settings are applied immediately and do not require a restart** + +## Dashboard / Relays + +[![](assets/img/screenshots/relays.jpeg)](assets/img/screenshots/relays.jpeg) + +- `Relay X Automatic Control: Connected Load (Watts)`: You can specify the resistive load power in watts connected to the relay. + If you do so, the relay will be activated automatically based on the grid power. +- `Relay X Manual Control`: ON/OFF switch to control the relay manually. Only available when the relay is not in automatic mode. Otherwise the relay state is displayed. + +**All these settings are applied immediately and do not require a restart** + +YaS☀️lR supports 2 relays (Electromechanical or SSR, controlled with 3.3V DC) to control external loads, or to be connected to the A1 and A2 terminals of a power contactor. +Relays can also be connected to the other resistance of the water tank (tri-phase resistance) as described in the [recommendations to reduce harmonics and flickering](./overview#recommendations-to-reduce-harmonics-and-flickering), in order to improve the routing and reduce harmonics. +You must use a SSR for that, because the relay will be switched on and off frequently. + +**The voltage is not dimmed**: these are 2 normal relays. + +These relays can also be **controlled manually**, from MQTT, REST API, Home Assistant, Jeedom, etc. + +**Pay attention that there is little to no hysteresis on the relays**. +So do not use the automatic feature to switch non-resistive loads such as pumps, electric vehicle chargers, etc. + +**If you need to switch other types of load** in a more complex way with some hysteresis or other complex conditions, you can use the MQTT, REST API, Home Assistant or Jeedom to query the `Virtual Power` metric and execute an automation based on this value. +The automation can then control the router relays remotely. The relays need to be set in `Manual Control`. + +Remember that **Solar Router's relays are not power contactors** and should not be used to directly control high power loads like an Electric Vehicle charge, a pump, etc. + +- **For an EV charge control**: an EV charging box which can dynamically change the charging current though a PWM signal (pilot wire) is recommended. OpenEVSE can do that and take as input the `Virtual Power` metric of this router to adjust the charging current. +- **For a pump**: a contactor is recommended which can be coupled with a Shelly EM to activate / deactivate the contactor remotely, and it can be automated by Home Assistant or Jeedom based on the `Virtual Power` metric of this router, but also the hours of day, days of week, depending on the weather, and of course with some hysteresis and safety mechanisms to force the pump ON or OFF depending on some rules. + +**Rules of Automatic Switching** + +`Grid Virtual Power` is calculated by the router as `Grid Power - Routed Power`. +This is the power that would be sent to the grid if the router was not routing any power to the resistive loads. + +`Grid Virtual Power` is negative on export and positive on import. + +- The relay will automatically start when `Grid Virtual Power + Relay Load <= -3% of Relay Load`. + In other words, the relay will automatically start when there is enough excess to absorb both the load connected to the relay plus 3% more of it. + When the relay will start, the remaining excess not absorbed by the load will be absorbed by the dimmer. + +- The relay will automatically stop when `Grid Virtual Power >= 3% of Relay Threshold`. + In other words, the relay will automatically stop when there is no excess anymore but a grid import equal to or more than 3% of the relay threshold. + When the relay will stop, there will be some excess again, which will be absorbed by the dimmer. + +For a 3000W tri-phase resistance, 3% means 30W per relay because there is 3x 1000W resistances. +For a 2100W tri-phase resistance, 3% means 21W per relay because there is 3x 700W resistances. + +## Dashboard / Management + +[![](assets/img/screenshots/management.jpeg)](assets/img/screenshots/management.jpeg) + +- `Configuration Backup`: Backup the current configuration of the router. +- `Configuration Restore`: Restore a previously saved configuration. +- `OTA Firmware Update`: Go to the firmware update page. +- `Restart`: Restart the router. +- `Debug`: Activate or deactivate debug logging. +- `Console`: Go to the Web Console page. +- `Energy Reset`: Reset the energy stored in all devices (JSY and PZEM) of the router. +- `Factory Reset`: Reset the router to factory settings and restart it. + +## Dashboard / Network + +[![](assets/img/screenshots/network.jpeg)](assets/img/screenshots/network.jpeg) + +- `Admin Password`: the password used to access (there is no password by default): + - Any Web page, including the [REST API](rest) + - The Access Point when activated + - The Captive Portal when the router restarts and no WiFi is available + +**Time settings:** + +- `NTP Server`: the NTP server to use to sync the time +- `Timezone`: the timezone to use for the router +- `Sync time with browser`: if the router does not have access to Internet or is not able to sync time (I.e. in AP mode), you can use this button to sync the time with your browser. + +**WiFi settings:** + +- `WiFi SSID`: the Home WiFi SSID to connect to +- `WiFi Password`: the Home WiFi password to connect to +- `Stay in AP Mode`: whether to activate or not the Access Point mode: switching the button will ask the router to stay in AP mode after reboot. + You will need to connect to its WiFi to access the dashboard again. + +**The ESP32 must be restarted to apply the changes.** + +## Dashboard / MQTT + +[![](assets/img/screenshots/mqtt.jpeg)](assets/img/screenshots/mqtt.jpeg) + +- `Server`: the MQTT broker address +- `Port`: the MQTT broker port (usually `1883` or `8883` for TLS) +- `Username`: the MQTT username +- `Password`: the MQTT password +- `SSL / TLS`: whether to use TLS or not (false by default). If yes, you must upload the server certificate. +- `Server Certificate`: when using SSL, you need to upload the server certificate. +- `Publish Interval`: the interval in seconds between each MQTT publication of the router data. + The default value is `5` seconds. +- `Base Topic`: the MQTT topic prefix to use for all the topics published by the router. + It is set by default to `yasolr_`. + I strongly recommend to keep this default value. The ID won't change except if you change the ESP board. + +**MQTT must be restarted to apply the changes.** + +### MQTT as a Grid Source + +- `Grid Voltage from MQTT Topic`: if set to a MQTT Topic, the router will listen to it to read the Grid voltage. + **Any measurement device (JSY or JSY Remote) will still have priority over MQTT**. + +- `Grid Power from MQTT Topic`: if set to a MQTT Topic, the router will listen to it to read the Grid power. + **It takes precedence over any other source, even a JSY connected to the ESP32**. + The reason is that it is impossible to know if the second channel of the JSY is really installed and used to monitor the grid power or not. + +**The ESP32 must be restarted to apply the changes.** + +MQTT topics are less accurate because depend on the refresh rate of this topic, and an expiration delay of a few seconds is set in order to stop any routing if no update is received in time. + +### Home Assistant Discovery + +YaS☀️lR supports Home Assistant Discovery: if configured, it will **automatically create a device** for the Solar Router in Home Assistant under the MQTT integration. + +| [![](assets/img/screenshots/ha_disco_1.jpeg)](assets/img/screenshots/ha_disco_1.jpeg) | [![](assets/img/screenshots/ha_disco_2.jpeg)](assets/img/screenshots/ha_disco_2.jpeg) | + +- `Home Assistant Integration`: whether to activate or not MQTT Discovery +- `Home Assistant Discovery Topic`: the MQTT topic prefix to use for all the topics published by the router for Home Assistant Discovery. + It is set by default to `homeassistant/discovery`. + I strongly recommend to keep this default value and configure Home Assistant to use this topic prefix for Discovery in order to separate state topics from discovery topics. + +**MQTT must be restarted to apply the changes.** + +The complete reference of the published data in MQTT is available [here](mqtt). +The published data can be explored with [MQTT Explorer](https://mqtt-explorer.com/). + +[![](assets/img/screenshots/mqtt_explorer.jpeg){: height="800" }](assets/img/screenshots/mqtt_explorer.jpeg) + +**Activating MQTT Discovery in Home Assistant** + +You can read more about Home Assistant Discovery and how to configure it [here](https://www.home-assistant.io/docs/mqtt/discovery/). + +Here is a configuration example for Home Assistant to move the published state topics under the `homeassistant/states`: + +```yaml +# https://www.home-assistant.io/integrations/mqtt_statestream +mqtt_statestream: + base_topic: homeassistant/states + publish_attributes: true + publish_timestamps: true + exclude: + domains: + - persistent_notification + - automation + - calendar + - device_tracker + - event + - geo_location + - media_player + - script + - update +``` + +To configure the discovery topic, you need to go to [http://homeassistant.local:8123/config/integrations/integration/mqtt](http://homeassistant.local:8123/config/integrations/integration/mqtt), then click on `configure`, then `reconfigure` then `next`, then you can enter the discovery prefix `homeassistant/discovery`. + +Once done on Home Assistant side and YaS☀️lR side, you should see the Solar Router device appear in Home Assistant in the list of MQTT devices. + +## Dashboard / GPIO + +[![](assets/img/screenshots/gpio.jpeg)](assets/img/screenshots/gpio.jpeg) + +This section allows to configure the pinout for the connected hardware and get some validation feedback. + +- Set the value to **-1** to disable the pin. +- Set the input to **blank** and save to reset the pin to its default value. + +If you see a warning with `(Input Only)`, it means that this configured pin can only be used to read +data. It perfectly OK for a ZCD, but you cannot use a pin that can only be read for a relay, DS18 sensor, etc. + +**If you change one of these settings, please stop and restart the corresponding Hardware.** + +## Dashboard / Hardware + +[![](assets/img/screenshots/hardware.jpeg)](assets/img/screenshots/hardware.jpeg) + +This section allows to enable / disable some features of the router, and get some feedback in case some activated features cannot be activated. + +All these components are activated **live without the need to restart the router**. + +## Dashboard / Hardware Config + +[![](assets/img/screenshots/hardware_config.jpeg)](assets/img/screenshots/hardware_config.jpeg) + +- `Nominal Grid Frequency`: the nominal grid frequency. +- `Display Type`: the type of display used. +- `Display Speed`: the speed at which the display will refresh to the next page. +- `Display Rotation`: the rotation of the display. +- `Output 1 PZEM Pairing`: starts the pairing procedure for Output 1 PZEM-004T v3 at address 0x01. +- `Output 2 PZEM Pairing`: starts the pairing procedure for Output 2 PZEM-004T v3 at address 0x02. +- `Output 1 Bypass Relay Type`: the type of relay for Output 1 Bypass: Normally Open or Normally Closed. +- `Output 2 Bypass Relay Type`: the type of relay for Output 2 Bypass: Normally Open or Normally Closed. +- `Relay 1 Type`: the type of relay for Relay 1: Normally Open or Normally Closed. +- `Relay 2 Type`: the type of relay for Relay 2: Normally Open or Normally Closed. + +**If you change one of these settings, please stop and restart the corresponding Hardware.** + +### Resistance Calibration + +The router **needs to know the resistance value of the load**. + +- `Output 1 Resistance`: the resistance value in Ohm of the load connected to Output 1 +- `Output 2 Resistance`: the resistance value in Ohm of the load connected to Output 2 + +Be careful to put a value that you have correctly measured! +An approximation will cause the router to not properly work because it won't be able to adjust the exact amount of power to send. + +If you have one of these device attached, they can help you. +You can set the dimmer in manual mode and set it to 50% and 100% and read the resistance values. +Then you just have to report it in the `Hardware Config` page. + +2. **PZEM-004T v3:** If you have wired a PZEM-004T v3 connected to each output, it will measure the resistance value when routing. + +3. **JSY-MK-194T:** If you have a JSY-MK-194T, you can activate the dimmer one by one to 100% and wait for the values to stabilize. + The router will then display the resistance value in the `Overview` page, thanks to the JSY. + +## Dashboard / PID Controller + +[![](assets/img/screenshots/pid_tuning.jpeg)](assets/img/screenshots/pid_tuning.jpeg) + +**For advanced users only.** + +This page allows to tune the PID algorithm used to control the automatic routing. +Use only if you know what you are doing and know how to tweak a PID controller. + +You can change the PID settings at runtime and the effect will appear immediately. +**If you find better settings, please do not hesitate to share them with the community.** + +`Real-time PID Data` can be activated to see the PID action in real time in teh graphs. +**Do not leave this option always activated because the data flow is so high that it impacts the ESP32 performance.** + +You are supposed to know how to tune a PID controller. +If not, please research on Google. +Here are some basic links to start with, which talks about the code used under the hood: + +- [Improving the Beginner’s PID – Introduction](http://brettbeauregard.com/blog/2011/04/improving-the-beginners-pid-introduction/) +- [Improving the Beginner’s PID – Derivative Kick](http://brettbeauregard.com/blog/2011/04/improving-the-beginners-pid-derivative-kick/) +- [Introducing Proportional On Measurement](http://brettbeauregard.com/blog/2017/06/introducing-proportional-on-measurement/) +- [Proportional on Measurement – The Code](http://brettbeauregard.com/blog/2017/06/proportional-on-measurement-the-code/) + +**Default Settings** + +- Proportional Mode: `On Input` +- Derivative Mode: `On Error` +- Integral Correction: `Anti-windup` +- Setpoint: `0` +- Kp: `0.3` +- Ki: `0.3` +- Kd: `0.1` +- Output Min: `-10000` +- Output Max: `10000` + +**PID Tuning through WebSocket** + +When `Real-time PID Data` is activated, a WebSocket endpoint is available at `/ws/pid/csv` and will stream all the PID data in real time in a `CSV` format when automatic dimmer control is activated. +You can quickly show then and process then in `bash` with `websocat` by typing for example: + +```bash +❯ websocat ws://192.168.125.123/ws/pid/csv +pMode,dMode,icMode,rev,setpoint,kp,ki,kd,outMin,outMax,input,output,error,sum,pTerm,iTerm,dTerm +2,1,2,0,800,0.300,0.400,0.200,-10000,10000,780.645,1109.700,19.355,1104.889,7.217,7.742,4.811 +2,1,2,0,800,0.300,0.400,0.200,-10000,10000,778.620,1114.453,21.380,1114.048,0.607,8.552,0.405 +2,1,2,0,800,0.300,0.400,0.200,-10000,10000,774.128,1126.643,25.872,1125.745,1.347,10.349,0.898 +2,1,2,0,800,0.300,0.400,0.200,-10000,10000,786.127,1125.294,13.873,1127.694,-3.600,5.549,-2.400 +2,1,2,0,800,0.300,0.400,0.200,-10000,10000,804.696,1116.531,-4.696,1120.245,-5.571,-1.878,-3.714 +2,1,2,0,800,0.300,0.400,0.200,-10000,10000,820.285,1104.337,-20.285,1107.455,-4.677,-8.114,-3.118 +2,1,2,0,800,0.300,0.400,0.200,-10000,10000,822.300,1097.527,-22.300,1097.930,-0.605,-8.920,-0.403 +2,1,2,0,800,0.300,0.400,0.200,-10000,10000,808.928,1101.045,-8.928,1098.370,4.012,-3.571,2.674 +2,1,2,0,800,0.300,0.400,0.200,-10000,10000,798.264,1104.396,1.736,1102.264,3.199,0.694,2.133 +2,1,2,0,800,0.300,0.400,0.200,-10000,10000,793.393,1107.342,6.607,1106.368,1.461,2.643,0.974 +2,1,2,0,800,0.300,0.400,0.200,-10000,10000,785.225,1116.361,14.775,1114.728,2.450,5.910,1.634 +2,1,2,0,800,0.300,0.400,0.200,-10000,10000,821.839,1087.686,-21.839,1095.008,-10.984,-8.736,-7.323 +``` + +You can also stream this data directly to a command-line tool that will plot in real time the graphs. +Example of such tools: + +- https://github.com/keithknott26/datadash +- https://github.com/cactusdynamics/wesplot + +**Demo** + +Here is a demo of the real-time PID tuning in action: + +[![PID Tuning in YaSolR (Yet Another Solar Router)](http://img.youtube.com/vi/ygSpUxKYlUE/0.jpg)](http://www.youtube.com/watch?v=ygSpUxKYlUE "PID Tuning in YaSolR (Yet Another Solar Router)") + +## Dashboard / Statistics + +[![](assets/img/screenshots/statistics.jpeg)](assets/img/screenshots/statistics.jpeg) + +This page shows a lot of statistics and information on the router. + +## Important Hardware Information + +### Bypass Relay + +Installing a relay for bypass is optional: if installed, the relay will be used to power the heater, and the dimmer will be set to 0. + +If not installed, when activating bypass mode, the dimmer will be used and set to 100%. +The advantage is a simple setup, the drawbacks are: + +- the dimmer will heat up. +- the power output of he dimmer counts as routed power so the routed power and energy will contain the bypass power also. + +### Display + +Supported displays are any I2C OLED Display of type `SSD1307`, `SH1106`, `SH1107`. + +`SH1106` is recommended and has been extensively tested. + +The display will look like a carousel with a maximum of 5 pages: + +- Global information +- Network information +- Router information with relays +- Output 1 information +- Output 2 information + +[![](assets/img/screenshots/display.gif)](assets/img/screenshots/display.gif) + +### JSY-MK-194T + +The JSY is used to measure: + +1. the grid power and voltage +2. the total routed power of the outputs combined (optional) + +The JSY can be replaced by MQTT, reading the power and voltage from MQTT topics. +See [MQTT as a Grid Source](#mqtt-as-a-grid-source). + +#### Remote JSY + +JSY can also be replaced with a remote JSY without any impact on routing speed. +You can install a JSY with an ESP32 on the electric panel and it will send its JSY data to the router remotely through an optimized communication protocol (UDP) several times per second. + +You can look in the [JSY project](https://oss.carbou.me/MycilaJSY/) to find more information about how to setup remote JSY and the supported protocols. +The `Sender` program is available at: + +[https://github.com/mathieucarbou/MycilaJSY/tree/main/examples/RemoteUDP](https://github.com/mathieucarbou/MycilaJSY/tree/main/examples/RemoteUDP) + +This is a standalone application that looks looks like this and will show all your JSY data, help you manage it, and also send the data through UDP. + +![](https://github.com/mathieucarbou/MycilaJSY/assets/61346/3066bf12-31d5-45de-9303-d810f14731d0) + +When using a remote JSY with the router, the following rules apply: + +- The voltage will always be read if possible from a connected JSY or PZEM, then from a remote JSY, then from MQTT. +- The grid power will always be read first from MQTT, then from a remote JSY, then from a connected JSY. + +**Speed** + +The JSY Remote through UDP is nearly as fast as having the JSY wired to the ESP. +All changes to the JSY are immediately sent through UDP to the listener at a rate of about **20 messages per second.** +This is the rate at which the JSY usually updates its data. + +### LEDs + +The LEDs are used to notify the user of some events like reset, restarts, router ready, routing, etc. + +| **LIGHTS** | **SOUNDS** | **STATES** | +| :--------: | ---------------- | ------------------------------- | +| `🟢 🟡 🔴` | `BEEP BEEP` | `STARTED` + `POWER` + `OFFLINE` | +| `🟢 🟡 ⚫` | | `STARTED` + `POWER` | +| `🟢 ⚫ 🔴` | `BEEP BEEP` | `STARTED` + `OFFLINE` | +| `🟢 ⚫ ⚫` | `BEEP` | `STARTED` | +| `⚫ 🟡 🔴` | `BEEP BEEP BEEP` | `RESET` | +| `⚫ 🟡 ⚫` | | | +| `⚫ ⚫ 🔴` | `BEEP BEEP` | `RESTART` | +| `⚫ ⚫ ⚫` | | `OFF` | + +- `STARTED`: application started and WiFi or AP mode connected +- `OFFLINE`: application disconnected from WiFi or disconnected from grid electricity +- `POWER`: power allowed to be sent (either through relays or dimmer) +- `RESTART`: application is restarting following a manual restart +- `RESET`: application is restarting following a manual reset +- `OFF`: application not working (power off) + +### PZEM-004T V3 + +Each output supports the addition of a PZEM-004T v3 sensor to monitor the power sent to the resistive load specifically for this output. +This also unlocks some additional features such as **independent outputs** and the ability to balance the excess power between outputs. + +Thanks to the PZEM per output, it is also possible to get some more precise information like the dimmed RMS voltage, resistance value, etc. + +#### Pairing procedure + +The PZEM-004T v3 devices has a special installation mode: you can install 2 PZEM-004T v3 devices on the same Serial TX/RX. +To communicate with the right one, each output will use a different slave address. +The initial setup requires to pair each PZEM-004T v3 with the corresponding output. + +1. Connect the 2 PZEM-004T v3 devices to the grid (L/N) and install the clamp around the wire at the exit of the dimmer of first output +2. Only connect the terminal wire (+5V, GND, RX, TX) of the first PZEM-004T v3 to pair to Output 1 +3. Boot the ESP32 wit the router firmware +4. Press the `PZEM Pairing` button in the `Output 1` menu +5. Verify that the pairing is successful +6. Disconnect the PZEM-004T v3 from the ESP32 +7. Connect the second PZEM (which has its clamp at the exit of the dimmer of the second output) to the ESP32 +8. Press the `PZEM Pairing` button in the `Output 2` menu this time +9. Verify that the pairing is successful +10. Now connect the 2 PZEM-004T v3 devices to the ESP32 + +You can verify that the pairing is successful by trying to activate the dimmer in the overview page, and see if you get the output power. + +Check also the logs in the Web Console at `http://yasolr-vwxyz.local/console` while doing the pairing procedure. + +This complex pairing procedure is not specific to this router project but is common to any PZEM-004T device when using several PZEM-004T v3 devices on the same Serial TX/RX. +You can read more at: + +- [mathieucarbou/MycilaPZEM004Tv3](https://github.com/mathieucarbou/MycilaPZEM004Tv3) +- [mandulaj/PZEM-004T-v30](https://github.com/mandulaj/PZEM-004T-v30) + +### Temperature Sensor + +The temperature sensors are used to monitor the water tank but also to trigger an automatic heating based on temperature levels (called **auto bypass**). + +Supported temperature sensor: `DS18B20` + +A temperature sensor can also be used to monitor the router itself. + +### Zero-Cross Detection + +The Zero-Cross Detection (ZCD) module is used to detect the zero-crossing of the grid voltage. +It is required, whether you use a Robodyn or SSR or any routing algorithm (phase control or burst mode). +The Robodyn includes a ZCD (its ZC pin). + +### Compatibility with EV box like OpenEVSE + +The router exposes through API and MQTT the **Virtual Grid Power**, which is the value of Grid Power you would have if the router was not routing. + +You can use this value to inject in the EV box in order to prioritize EV charging over routing to the water tank. + +This is usually acceptable to give the EV box a priority over the water tank, because the water tank only need a small amount of routed energy to start heating, while the EV usually requires a threshold to start charging. +So the router will take whatever is not used by the EV box. + +## Help and support + +- **Facebook Group**: [https://www.facebook.com/groups/yasolr](https://www.facebook.com/groups/yasolr) + +- **GitHub Discussions**: [https://github.com/mathieucarbou/YaSolR-OSS/discussions](https://github.com/mathieucarbou/YaSolR-OSS/discussions) + +- **GitHub Issues**: [https://github.com/mathieucarbou/YaSolR-OSS/issues](https://github.com/mathieucarbou/YaSolR-OSS/issues) + +``` + +``` diff --git a/docs/mqtt.md b/docs/mqtt.md new file mode 100644 index 0000000..328f4e4 --- /dev/null +++ b/docs/mqtt.md @@ -0,0 +1,285 @@ +--- +layout: default +title: MQTT API +description: MQTT API +--- + +# MQTT Topics + +- [`/status`](#status) +- [`/config`](#config) +- [`/grid`](#grid) +- [`/router`](#router) +- [`/router/outputX`](#routeroutputx) +- [`/router/outputX/dimmer`](#routeroutputxdimmer) +- [`/router/outputX/relay`](#routeroutputxrelay) +- [`/router/relayX`](#routerrelayx) +- [`/system/app`](#systemapp) +- [`/system/device`](#systemdevice) +- [`/system/device/restart`](#systemdevicerestart) +- [`/system/device/heap`](#systemdeviceheap) +- [`/system/firmware`](#systemfirmware) +- [`/system/firmware/build`](#systemfirmwarebuild) +- [`/system/network`](#systemnetwork) +- [`/system/network/eth`](#systemnetworketh) +- [`/system/network/wifi`](#systemnetworkwifi) + +Not everything MQTT topic will update frequently (5 sec by default). +Some topics, like configuration related, will only update when the configuration is changed. +These will have the retain flag set to true so that a subscriber coming after the data was published will still get the update. + +## `/status` + +This is the will topic which can be used to detect when the device is connected or gone. +It is also used by Home Assistant discovery. +It is set to `online` or `offline` + +## `/config` + +```properties +admin_pwd = ******** +ap_mode_enable = false +debug_enable = true +disp_angle = 0 +disp_enable = true +disp_speed = 3 +disp_type = SH1106 +ds18_sys_enable = true +grid_freq = 50 +grid_pow_mqtt = homeassistant/states/sensor/grid_power/state +grid_volt = 230 +grid_volt_mqtt = +ha_disco_enable = true +ha_disco_topic = homeassistant/discovery +jsy_enable = true +lights_enable = true +mqtt_enable = true +mqtt_port = 1883 +mqtt_pub_itvl = 5 +mqtt_pwd = ******** +mqtt_secure = false +mqtt_server = 192.168.125.90 +mqtt_topic = yasolr_a1c48 +mqtt_user = homeassistant +ntp_server = pool.ntp.org +ntp_timezone = Europe/Paris +o1_ab_enable = false +o1_ad_enable = false +o1_days = sun,mon,tue,wed,thu,fri,sat +o1_dim_enable = true +o1_dim_limit = 100 +o1_ds18_enable = true +o1_pzem_enable = true +o1_relay_enable = true +o1_relay_type = NO +o1_temp_start = 50 +o1_temp_stop = 60 +o1_time_start = 22:00 +o1_time_stop = 06:00 +o2_ab_enable = false +o2_ad_enable = false +o2_days = sun,mon,tue,wed,thu,fri,sat +o2_dim_enable = true +o2_dim_limit = 27 +o2_ds18_enable = true +o2_pzem_enable = true +o2_relay_enable = false +o2_relay_type = NO +o2_temp_start = 50 +o2_temp_stop = 60 +o2_time_start = 22:00 +o2_time_stop = 06:00 +pin_disp_scl = 22 +pin_disp_sda = 21 +pin_ds18 = 4 +pin_jsy_rx = 16 +pin_jsy_tx = 17 +pin_lights_g = 0 +pin_lights_r = 15 +pin_lights_y = 2 +pin_o1_dim = 25 +pin_o1_ds18 = 18 +pin_o1_relay = 32 +pin_o2_dim = 26 +pin_o2_ds18 = 5 +pin_o2_relay = 33 +pin_pzem_rx = 14 +pin_pzem_tx = 27 +pin_relay1 = 13 +pin_relay2 = 12 +pin_zcd = 35 +relay1_enable = true +relay1_load = 0 +relay1_type = NO +relay2_enable = true +relay2_load = 0 +relay2_type = NO +wifi_pwd = +wifi_ssid = IoT +zcd_enable = true +``` + +**Update** + +```properties +# Set a configuration key to a new value +/config//set +``` + +## `/grid` + +```properties +apparent_power = 0 +current = 0 +energy = 0 +energy_returned = 0 +frequency = 49.97999954 +online = true +power = 617.6273804 +power_factor = 0 +voltage = 234.0296021 +``` + +## `/router` + +```properties +apparent_power = 0 +current = 0 +energy = 0.067999996 +lights = 🟢 🟡 ⚫ +power = 239.4906921 +power_factor = 0.495258361 +thdi = 1.550398946 +temperature = 26.30999947 +virtual_grid_power = -503.7492371 +``` + +## `/router/outputX` + +```properties +bypass = on +state = Idle +temperature = 26.3 +``` + +**Update** + +```properties +# Switch bypass on or off +/router/outputX/bypass/set = "on" +/router/outputX/bypass/set = "off" +``` + +## `/router/outputX/dimmer` + +```properties +duty = 1000 +duty_cycle = 100 +state = on +``` + +**Update** + +```properties +# Update the dimmer duty / duty cycle +/router/outputX/dimmer/duty/set = [0, 4095] +/router/outputX/dimmer/duty_cycle/set = [0.0, 100.0] +``` + +## `/router/outputX/relay` + +```properties +state = off +switch_count = 2 +``` + +## `/router/relayX` + +```properties +state = off +switch_count = 2 +``` + +**Update** + +```properties +# Switch relay on or off or for a duration in milliseconds +/router/relayX/state/set = "on" +/router/relayX/state/set = "on=5000" +/router/relayX/state/set = "off" +``` + +## `/system/app` + +```properties +manufacturer = Mathieu Carbou +model = Pro +name = YaSolR +version = main_0ed7d852_modified +``` + +## `/system/device` + +```properties +boots = 1 +cores = 2 +cpu_freq = 240 +id = A1C48 +model = ESP32-D0WD-V3 +uptime = 1675 +``` + +## `/system/device/restart` + +Restarts tge device + +## `/system/device/heap` + +```properties +total = 269404 +usage = 47.63 +used = 128316 +``` + +## `/system/firmware` + +```properties +debug = true +filename = YaSolR-main-pro-esp32-debug.bin +``` + +## `/system/firmware/build` + +```properties +branch = main +hash = da49a3a +timestamp = 2024-06-08T12:14:30.915965+00:00 +``` + +## `/system/network` + +```properties +hostname = yasolr-a1c48 +ip_address = 192.168.125.121 +mac_address = B0:B2:1C:0A:1C:48 +mode = wifi +ntp = on +``` + +## `/system/network/eth` + +```properties +ip_address = 0.0.0.0 +mac_address = B0:B2:1C:0A:1C:50 +``` + +## `/system/network/wifi` + +```properties +bssid = 00:17:13:37:28:C0 +ip_address = 0.0.0.0 +mac_address = B0:B2:1C:0A:1C:50 +quality = 100 +rssi = -21 +ssid = IoT +``` diff --git a/docs/overview.md b/docs/overview.md new file mode 100644 index 0000000..f4ffd27 --- /dev/null +++ b/docs/overview.md @@ -0,0 +1,229 @@ +--- +layout: default +title: Overview +description: Overview +--- + +# Overview + +- [What is a Solar Router ?](#what-is-a-solar-router-) +- [How a Solar Router work ?](#how-a-solar-router-work-) + - [Zero-Cross Detection (ZCD)](#zero-cross-detection-zcd) + - [Robodyn and Solid State Relay (SSR)](#robodyn-and-solid-state-relay-ssr) +- [Phase Control](#phase-control) + - [Harmonics](#harmonics) +- [Burst Fire Control](#burst-fire-control) + - [Flickering](#flickering) +- [Recommendations to reduce harmonics and flickering](#recommendations-to-reduce-harmonics-and-flickering) +- [References](#references) + +## What is a Solar Router ? + +A _Solar Router_ allows to redirect the solar production excess to some appliances instead of returning it to the grid. +The particularity of a solar router is that it will dim the voltage and power sent to the appliance in order to match the excess production, in contrary to a simple relay that would just switch on/off the appliance without controlling its power. + +A _Solar Router_ is usually connected to the resistance of a water tank and will heat the water when there is production excess. + +A solar router can also do more things, like controlling (on/off) the activation of other appliances (with the grid normal voltage and not the dimmed voltage) in case the excess reaches a threshold. For example, one could activate a pump, pool heater, etc if the excess goes above a specific amount, so that this appliance gets the priority over heating the water tank. + +A router can also schedule some forced heating of the water tank to ensure the water reaches a safe temperature, and consequently bypass the dimmed voltage. This is called a bypass relay. + +## How a Solar Router work ? + +A router is composed of 2 main pieces: + +1. **A bi-directional measurement system** that will detect the solar production excess and the home consumption in Watts: Linky TIC, Shelly EM, JSY, etc + +2. A **dimmer system** that will control the voltage and power sent to the resistance of the water tank to match the measured excess: Robodyn AC Dimmer, Random Solid State Relay, etc + +The dimmer systems are usually based on TRIAC / Thyristors and are controlling the power through different methods: + +| **Phase Control** | **Burst Fire Control** | +| :-----------------------------------------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | +| ![](assets/img/measurements/Oscillo_Dimmer_50.jpeg) | ![](assets/img/measurements/Burst_50.png) | +| In this mode, the TRIAC lets the current pass at a specific moment within the 2 semi-periods of 10 ms by "cutting" the sin wave | In this mode, the TRIAC is used like a rapid switch, to let pass a sequence of complete full periods or semi-periods without cutting them | +| **Used devices:**
Robodyn, Random SSR | **Used devices:**
Robodyn, Random SSR, Zero-Cross SSR | +| **Pros:**
More precise routing, can control exactly the right amount of power to let pass | **Pros:**
Easier to grasp and implement and does not create harmonics | +| **Cons:**
Can cause harmonics that can be difficult to filter out (but effect can be limited or mitigated) | **Cons:**
Less precise routing because each complete period (or semi-period) lets the full power (or half power) pass, and can cause flickering (light bulbs that are close-by can blink because of the fast and successive current switches) | + +Other algorithms exist, more or less complex but generally based on these 2 methods. + +### Zero-Cross Detection (ZCD) + +To know when to switch or cut the voltage wave, routers are using a **Zero-Cross Detection (ZCD) circuit** that will detect when the voltage curve crosses the Zero point (which is twice per period) and will send a pulse to the controller board. +Here are below some examples of how a ZCD circuit works by looking at 2 different implementations: Robodyn and a more specific one from [Daniel S.](https://www.pcbway.com/project/shareproject/Zero_Cross_Detector_a707a878.html). + +| **Dedicated ZCD circuit** | **Robodyn ZCD circuit** | +| :------------------------------------------------------------------------------------------: | :----------------------------------------------------------------------------------------------------------: | +| [![ZCD](assets/img/measurements/Oscillo_ZCD.jpeg)](assets/img/measurements/Oscillo_ZCD.jpeg) | [![ZCD](assets/img/measurements/Oscillo_ZCD_Robodyn.jpeg)](assets/img/measurements/Oscillo_ZCD_Robodyn.jpeg) | + +When the AC voltage curve crosses the Zero point, the ZCD circuit sends a pulse (with a custom duration) to the controller board, which now knows that the voltage is at zero. +The board then does some calculation to determine when to send the signal to the TRIAC (or Random SSR or Robodyn) to activate it, based on the excess power, or if using burst fire control, to know when to let the current pass and for how many semi-periods. + +### Robodyn and Solid State Relay (SSR) + +A Solid State Relay is a relay that does not have any moving parts and is based on a semiconductor. +It can be turned on and off very fast. + +A **Zero-Cross SSR** is a relay that will only close or open when the voltage curve is at 0. +It won't generate any harmonics and is not able to do Phase Control, but it can be used for Burst Fire Control. + +A **Random** SSR is a relay that can be turned on and off at any point in time, at any voltage level. +It can be used for Phase Control and Burst Fire Control. +If activated when the voltage curve is not at 0, it will generate harmonics. + +Due to the nature of SSR, the more they are used (switched on/off), the more they will heat up. +So it is recommended to install them on a vertical heat sink. + +SSR also have some specifications to take into account for the use of a Solar Router: + +- **Type of control**: DA: (DC Control AC) +- **Control voltage**: 3.3V should be in the range (example: 3-32V DC) + +**Robodyn** is a device that includes both a ZCD circuit and a TRIAC, which makes it ideal for a Solar Router using Phase Control System. +Using a Random SSR instead of the Robodyn is possible but will require the use of an additional ZCD circuit. + +## Phase Control + +**Effect on current** + +In this mode, the TRIAC lets the current pass at a specific moment within the 2 semi-periods of 10 ms by "cutting" the sin wave. + +Here are 3 different views from an Owon VDS6104 oscilloscope of: + +1. The AC voltage at dimmer input (red) +2. The ZCD pulse detected by the ESP32 board when the voltage crosses the Zero line (yellow) +3. The control voltage of the random SSR (or Robodyn) that is sent from the ESP32 board. The TRIAC will let the current pass when the control voltage is >= 3.3V (blue) +4. The dimmed AC current at dimmer output (pink) + +| | **Dimmer Duty 10 / 4095** | **Dimmer Duty 2047 / 4095 (50%)** | **Dimmer Duty 4090 / 4095** | +| :-------------------: | :---------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------: | +| **Robodyn** | [![](assets/img/measurements/Robodyn_duty_10.png)](assets/img/measurements/Robodyn_duty_10.png) | [![](assets/img/measurements/Robodyn_duty_2047.png)](assets/img/measurements/Robodyn_duty_2047.png) | [![](assets/img/measurements/Robodyn_duty_4090.png)](assets/img/measurements/Robodyn_duty_4090.png) | +| **Better ZCD Module** | [![](assets/img/measurements/ZCD_duty_10.png)](assets/img/measurements/ZCD_duty_10.png) | [![](assets/img/measurements/ZCD_duty_2047.png)](assets/img/measurements/ZCD_duty_2047.png) | [![](assets/img/measurements/ZCD_duty_4090.png)](assets/img/measurements/ZCD_duty_4090.png) | + +Dimmer at 50% matches the 90 degrees angle of the voltage curve, so the current is chopped at 50% of the period. This is when the harmonic level is the highest. +We can clearly see the effect of the TRIAC on the voltage curve, and the resulting current curve, which is chopped at the wanted level. + +Robodyn as a poor ZCD signal. +If you can, take a better ZCD module. +This will also help ZCD edge detection because the ESP32 is subject to [spurious interrupt issue](https://github.com/fabianoriccardi/dimmable-light/wiki/Notes-about-specific-architectures#interrupt-issue) when detecting ZCD edges. +Hopefully this can be overcome by filtering out the noise in the code. + +**Effect on voltage** + +Now, let's see what is happening to the input and output voltage when the dimmer is at 20% with a current of about 1.7A. +Measurements are done with the Owon HDS2202S: voltage in yellow and current in blue. + +| **At Router Input** | **At Router Output** | +| :--------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------: | +| [![](assets/img/measurements/Oscillo_Dim_20_In.jpeg)](assets/img/measurements/Oscillo_Dim_20_In.jpeg) | [![](assets/img/measurements/Oscillo_Dim_20_Out.jpeg)](assets/img/measurements/Oscillo_Dim_20_Out.jpeg) | +| At Router input, before the dimmer, the voltage curve is normal. But the requested current shape is as defined by the TRIAC activations. | At Router output, after the dimmer, both the voltage and current curves are chopped according to the TRIAC activation | + +### Harmonics + +The biggest issue with Phase Control is that consecutively chopping the voltage curve creates some spontaneous current request especially at the point when the voltage at its minimum or maximum. +This could be compared to suddenly opening a water valve instead of gradually opening it. The pressure is higher and the water flow will be more turbulent. +These harmonics are bigger when the TRIAC lets the current pass at 90 degrees (50% of the nominal power). + +Harmonics are causing a distortion of the voltage and current curves, and are transporting some energy at higher frequencies, multiples of the fundamental frequency (50Hz in Europe, 60Hz in the US). +The power factor is also impacted by harmonics: the more harmonics there are, the more the power factor will be degraded. + +![](assets/img/measurements/Harmoniques.jpeg) + +Harmonics are not bad, but they can be damaging for some appliances if they are too high, such as motors, UPS, electronic devices, etc. +Harmonics are also regulated according to [CEI 61000-3-2](http://crochet.david.online.fr/bep/copie%20serveur/Normes/cei%2061000-3-2.pdf) (A Solar Router is a Class A device). +This document gives the maximum current allowed for each harmonic level. + +[![](assets/img/measurements/CEI%2061000-3-2.png)](assets/img/measurements/CEI%2061000-3-2.png) + +Some studies were done to determine the level of harmonics a Solar Router would generate and at which power. Here are some key things to consider: + +- The worst case scenario is when the TRIAC angle is at 90 degrees (50% power) +- The Harmonic #15 is the first harmonic level to be reached with a nominal load of about **760 W** +- But H15 is insignificant compared to H3, which is the most significant one in terms of energy transported +- The Harmonic #3 maximum level according to CEI 61000-3-2 is reached with a nominal load of about **1700 W** +- To stay compliant with CEI 61000-3-2, the maximum nominal load should be less than 800 W + +| **Harmonic #3** | **Harmonic #15** | +| :-------------------------------------------------------------------: | :---------------------------------------------------------------------: | +| [![](assets/img/measurements/H3.png)](assets/img/measurements/H3.png) | [![](assets/img/measurements/H15.png)](assets/img/measurements/H15.png) | + +To put things in perspective, it is important to remember that a Solar Router will adapt the TRIAC angle based on the excess power, so **the router will not always be dimming at 90 degrees**, at the worst case scenario. + +## Burst Fire Control + +Burst Fire Control will let a complete or half complete voltage curve pass or not, and this control is done from the zero point up to the next. +So the sin wave is not chopped like in Phase Control, but we decide to let pass or not a complete period or half period. + +50Hz current has a voltage curve with a period of 20 ms decoupled in 2 half-periods: one positive, one negative, so the zero voltage is crossed twice per period. +A measuring device like JSY makes about 300 ms to see a new value, which gives us 15 full periods and 30 half-periods to re-arrange the current flow, before the next measurement. + +This method can use a simple Zero-Cross Solid State Relay: a relay that will only close or open when the voltage curve is at 0. +So there is no load at that time of switching, thus, no harmonics. + +**This method is not as accurate as Phase Control, but still provides good results, depending on the load.** + +### Flickering + +The main problem with Burst Fire Control is that some kind of arrangements can cause flickering when the nominal load is big (big power tanks), visible on light bulbs that are close-by. +This is caused by a sudden voltage drop in the house, caused by a sudden current flow at the request of the big water tank resistance. + +## Recommendations to reduce harmonics and flickering + +Harmonics and flickering cannot be completely avoided but they can be mitigated or limited by following some recommendations: + +1. YaSolR has a "limiter" to prevent the router from dimming more than the limit set. + For example, if you have a nominal load of 3000W and set the limiter to 50%, the router will not use more than 1500W of excess. + +2. Reduce cable length between the router and the resistance to the bare minimum (a few meters) and use wider cables + +3. Remove any sensitive equipment close to wires going in the router and out of the router + +4. Put your router and resistance circuit as close as possible to the grid entrance and exit (eventually by re-arranging the DIN rail) + +5. Change your water tank resistance to a resistance with less nominal load (more ohms) in order to decrease the current load. + For example, a 3000W resistance with 18 ohms will have a current load of 7.4A when dimmed at 134V for 1000W of excess and will generate more harmonics or will cause more flickering. + While a 1000W resistance with 53 ohms will have a current load of only 4.3A and will be used at full power. + +6. Switch your water tank resistance with a a tri-phase resistance in order to be able to control 3 resistances in steps independently: they will be activated step by step. Example: + + - Resistance 1: 800 W connected to the dimmer: the dimmer will route the solar excess from 0 to 800 W to this resistance + - Resistance 2: 800 W connected to relay 1: when the excess is above 800 W, the relay will activate (all or nothing relay) and the second resistance will receive 800 W of excess. + The first resistance connected to the Phase Control system (dimmer or Random SSR) will receive what is remaining from the excess. + - Resistance 3: 800 W connected to relay 2: will work the same and will be activated when the excess will be above 800W. + +7. If some devices are impacted by harmonics (such as an EV box restarting), try put an RC Snubber at your dimmer output between phase (going to the load) and neutral. + It won't solve the harmonic issue but can help mitigate equipments sensible to energy spikes. + Suggestions often seen are to use a 100 ohms 0.1uF (100nF) RC Snubber (like the ones sold for Shelly devices). + +## References + +**Solar Routers** + +- [Avantage d'un routeur solaire](https://sites.google.com/view/le-professolaire/routeur-professolaire) (Anthony G., _Le Profes'Solaire_) +- [Principe du routeur photovoltaïque](https://f1atb.fr/fr/realisation-dun-routeur-photovoltaique-multi-sources-multi-modes-et-modulaire/) (André B., _[F1ATB](https://github.com/F1ATB)_) + +**YouTube videos** explaining the theory behind a Solar Router, harmonics and solutions, with some simulations and practical examples. + +- [Installations photovoltaïques](https://www.youtube.com/playlist?list=PLWpzro3Ndk_2PUlQkULUjP6VSzwmFXkPc) (Pierre Chfd) +- [Routeur solaire ongrid](https://www.youtube.com/playlist?list=PL-IXE4AO5wkuxvQLEB-AuwoxZF1ZRzClf) (Sébastien P., _[SeByDocKy](https://github.com/SeByDocKy)_) + +**Harmonics** + +- [Etude des harmoniques du courant de ligne](https://www.thierry-lequeu.fr/data/TRIAC.pdf) (Thierry Lequeu) +- [Détection et atténuation des harmoniques](https://fr.electrical-installation.org/frwiki/Détection_et_atténuation_des_harmoniques) (Schneider Electric) +- [Router via TRIAC et "Pollution" du réseau](https://forum-photovoltaique.fr/viewtopic.php?t=60521) (Forum photovoltaïque) +- [HARMONICS: CAUSES, EFFECTS AND MINIMIZATION](https://www.salicru.com/files/pagina/72/278/jn004a01_whitepaper-armonics_%281%29.pdf) (Ramon Pinyol, R&D Product Leader SALICRU) +- [HARMONIQUES ET DEPOLLUTION DU RESEAU ELECTRIQUE](http://archives.univ-biskra.dz/bitstream/123456789/21913/1/BELHADJ%20KHEIRA%20ET%20BOUZIR%20NESSRINE.pdf) (BELHADJ KHEIRA ET BOUZIR NESSRINE) +- [Impact de la pollution harmonique sur les matériels de réseau](https://theses.hal.science/tel-00441877/document) (Wilfried Frelin) + +**TRIAC** + +- [NEW TRIACS: IS THE SNUBBER CIRCUIT NECESSARY?](https://www.thierry-lequeu.fr/data/AN437.pdf) (Thierry Lequeu) +- [Le triac](https://emrecmic.wordpress.com/2017/02/07/le-triac/) +- [Le gradateur](http://philippe.demerliac.free.fr/RichardK/Graduateur.pdf) (Richard KOWAL) + +**Technical docs and algorithms** + +- [Learn: PV Diversion](https://docs.openenergymonitor.org/pv-diversion/) +- [Optimized Random Integral Wave AC Control Algorithm for AC heaters](https://tsltd.github.io) diff --git a/docs/rest.md b/docs/rest.md new file mode 100644 index 0000000..5120098 --- /dev/null +++ b/docs/rest.md @@ -0,0 +1,363 @@ +--- +layout: default +title: HTTP API +description: HTTP API +--- + +# Web Endpoints + +- [`/api`](#api) +- [`/api/config`](#apiconfig) +- [`/api/config/backup`](#apiconfigbackup) +- [`/api/debug`](#apidebug) +- [`/api/grid`](#apigrid) +- [`/api/router`](#apirouter) +- [`/api/system`](#apisystem) +- [`/api/system/reset`](#apisystemreset) +- [`/api/restart`](#apirestart) + +## `/api` + +List all available endpoints + +```bash +curl -X GET http:///api +``` + +```json +{ + "config": "http://192.168.125.121/api/config", + "config/backup": "http://192.168.125.121/api/config/backup", + "debug": "http://192.168.125.121/api/debug", + "grid": "http://192.168.125.121/api/grid", + "router": "http://192.168.125.121/api/router", + "system": "http://192.168.125.121/api/system", + "system/restart": "http://192.168.125.121/api/system/restart" +} +``` + +## `/api/config` + +Configuration view, update, backup and restore + +```bash +curl -X GET http:///api/config +``` + +```json +{ + "admin_pwd": "", + "ap_mode_enable": "false", + "debug_enable": "true", + "disp_angle": "0", + "disp_enable": "true", + "disp_speed": "3", + "disp_type": "SH1106", + "ds18_sys_enable": "true", + "grid_freq": "50", + "grid_pow_mqtt": "homeassistant/states/sensor/grid_power/state", + "grid_volt": "230", + "grid_volt_mqtt": "", + "ha_disco_enable": "true", + "ha_disco_topic": "homeassistant/discovery", + "jsy_enable": "true", + "lights_enable": "true", + "mqtt_enable": "true", + "mqtt_port": "1883", + "mqtt_pub_itvl": "5", + "mqtt_pwd": "********", + "mqtt_secure": "false", + "mqtt_server": "192.168.125.90", + "mqtt_topic": "yasolr_a1c48", + "mqtt_user": "homeassistant", + "ntp_server": "pool.ntp.org", + "ntp_timezone": "Europe/Paris", + "o1_ab_enable": "false", + "o1_ad_enable": "false", + "o1_days": "sun,mon,tue,wed,thu,fri,sat", + "o1_dim_enable": "true", + "o1_dim_limit": "100", + "o1_ds18_enable": "true", + "o1_pzem_enable": "true", + "o1_relay_enable": "true", + "o1_relay_type": "NO", + "o1_temp_start": "50", + "o1_temp_stop": "60", + "o1_time_start": "22:00", + "o1_time_stop": "06:00", + "o2_ab_enable": "false", + "o2_ad_enable": "false", + "o2_days": "sun,mon,tue,wed,thu,fri,sat", + "o2_dim_enable": "true", + "o2_dim_limit": "27", + "o2_ds18_enable": "true", + "o2_pzem_enable": "true", + "o2_relay_enable": "false", + "o2_relay_type": "NO", + "o2_temp_start": "50", + "o2_temp_stop": "60", + "o2_time_start": "22:00", + "o2_time_stop": "06:00", + "pin_disp_scl": "22", + "pin_disp_sda": "21", + "pin_ds18": "4", + "pin_jsy_rx": "16", + "pin_jsy_tx": "17", + "pin_lights_g": "0", + "pin_lights_r": "15", + "pin_lights_y": "2", + "pin_o1_dim": "25", + "pin_o1_ds18": "18", + "pin_o1_relay": "32", + "pin_o2_dim": "26", + "pin_o2_ds18": "5", + "pin_o2_relay": "33", + "pin_pzem_rx": "14", + "pin_pzem_tx": "27", + "pin_relay1": "13", + "pin_relay2": "12", + "pin_zcd": "35", + "relay1_enable": "true", + "relay1_load": "0", + "relay1_type": "NO", + "relay2_enable": "true", + "relay2_load": "0", + "relay2_type": "NO", + "wifi_pwd": "", + "wifi_ssid": "IoT", + "zcd_enable": "true" +} +``` + +```bash +# Configuration Update: +curl -X POST \ + -F "hostname=foobarbaz" \ + -F "admin_password=" \ + -F "ntp_server=fr.pool.ntp.org" \ + -F "ntp_timezone=Europe/Paris" \ + [...] + http:///api/config +``` + +## `/api/config/backup` + +```bash +# Backup configuration config.txt: +curl -X GET http:///api/config/backup +``` + +```bash +# Restore configuration config.txt: +curl -X POST -F "data=@./path/to/config.txt" http:///api/config/restore +``` + +## `/api/debug` + +Display many internal information about each hardware component, system and tasks + +```bash +curl -X GET http:///api/debug +``` + +## `/api/grid` + +Display grid electricity information + +```bash +curl -X GET http:///api/grid +``` + +```json +{ + "apparent_power": 0, + "current": 0, + "energy": 0.029999999, + "energy_returned": 0, + "frequency": 49.97999954, + "online": true, + "power": 723.661377, + "power_factor": 0, + "voltage": 233.9530029 +} +``` + +## `/api/router` + +Show the router information and allows to control the relays, dimmers and bypass + +```bash +curl -X GET http:///api/router +``` + +```json +{ + "energy": 0.193000004, + "lights": "🟢 🟡 ⚫", + "power": 334.1828918, + "power_factor": 0.726000011, + "temperature": 24.80999947, + "thdi": 1.550398946, + "virtual_grid_power": 671.1778564, + "output1": { + "bypass": "off", + "state": "Routing", + "temperature": 24.05999947, + "dimmer": { + "duty": 2502, + "duty_cycle": 61.09890366, + "state": "on" + }, + "metrics": { + "apparent_power": 0, + "current": 0, + "energy": 0.101000004, + "power": 0, + "power_factor": 0.781657875, + "resistance": 0, + "thdi": 1.550398946, + "voltage_dimmed": 180.6411438 + }, + "relay": { + "state": "off", + "switch_count": 0 + } + }, + "output2": { + "bypass": "off", + "state": "Routing", + "temperature": 24.05999947, + "dimmer": { + "duty": 2502, + "duty_cycle": 61.09890366, + "state": "on" + }, + "metrics": { + "apparent_power": 0, + "current": 0, + "energy": 0.092, + "power": 0, + "power_factor": 0.541782916, + "resistance": 0, + "thdi": 1.550398946, + "voltage_dimmed": 125.3685608 + }, + "relay": { + "state": "off", + "switch_count": 0 + } + }, + "relay1": { + "state": "off", + "switch_count": 0 + }, + "relay2": { + "state": "off", + "switch_count": 0 + } +} +``` + +```bash +# Change relay state for a specific duration (duration is optional) +curl -X POST \ + -F "state=on" \ + -F "duration=20000" \ + http:///api/router/relay1 +``` + +```bash +# Set the duty of the dimmer +curl -X POST \ + -F "duty=4095" \ + http:///api/router/output1/dimmer +``` + +```bash +# Set the duty cycle of the dimmer [0.0, 100.0] +curl -X POST \ + -F "duty_cycle=50.55" \ + http:///api/router/output1/dimmer +``` + +```bash +# Change bypass relay state +curl -X POST \ + -F "state=on" \ + http:///api/router/output1/bypass +``` + +## `/api/system` + +System information: device, memory usage, network, application, router temperature, etc + +```bash +curl -X GET http:///api/system +``` + +```json +{ + "app": { + "manufacturer": "Mathieu Carbou", + "model": "Pro", + "name": "YaSolR", + "version": "main_927a10c_modified" + }, + "device": { + "boots": 980, + "cores": 2, + "cpu_freq": 240, + "heap": { + "total": 272852, + "usage": 47.74000168, + "used": 130260 + }, + "id": "A1C48", + "model": "ESP32-D0WD", + "revision": 301, + "uptime": 5109 + }, + "firmware": { + "build": { + "branch": "main", + "hash": "927a10c", + "timestamp": "2024-06-08T10:17:11.607303+00:00" + }, + "debug": true, + "filename": "YaSolR-main-pro-esp32-debug.bin" + }, + "network": { + "eth": { + "ip_address": "0.0.0.0", + "mac_address": "" + }, + "hostname": "yasolr-a1c48", + "ip_address": "192.168.125.121", + "mac_address": "B0:B2:1C:0A:1C:48", + "mode": "wifi", + "ntp": "on", + "wifi": { + "bssid": "00:17:13:37:28:C0", + "ip_address": "192.168.125.121", + "mac_address": "B0:B2:1C:0A:1C:48", + "quality": 91, + "rssi": -35, + "ssid": "IoT" + } + } +} +``` + +## `/api/system/reset` + +```bash +# System Restart +curl -X POST http:///api/system/restart +``` + +## `/api/restart` + +```bash +# System Factory Reset +curl -X POST http:///api/system/reset +``` diff --git "a/docs/routers/F1ATB/R\303\251alisation d\342\200\231un Routeur photovolta\303\257que Multi-Sources Multi-Modes et Modulaire \342\200\223 F1ATB.pdf" "b/docs/routers/F1ATB/R\303\251alisation d\342\200\231un Routeur photovolta\303\257que Multi-Sources Multi-Modes et Modulaire \342\200\223 F1ATB.pdf" new file mode 100644 index 0000000..1b30c5f Binary files /dev/null and "b/docs/routers/F1ATB/R\303\251alisation d\342\200\231un Routeur photovolta\303\257que Multi-Sources Multi-Modes et Modulaire \342\200\223 F1ATB.pdf" differ diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/Actions.cpp b/docs/routers/F1ATB/Solar_Router_V10.00/Actions.cpp new file mode 100644 index 0000000..5a254d1 --- /dev/null +++ b/docs/routers/F1ATB/Solar_Router_V10.00/Actions.cpp @@ -0,0 +1,238 @@ +// ******************** +// Gestion des Actions +// ******************** +#include +#include "Actions.h" +#include "EEPROM.h" +#include + + + +//Class Action +Action::Action() { + Gpio = -1; +} +Action::Action(int aIdx) { + Gpio = -1; // si le n° de pin n'est pas valid, on ne fait rien + Idx = aIdx; + T_LastAction = int(millis() / 1000); + On = false; + Actif = 0; + Reactivite = 50; + OutOn = 1; + OutOff = 0; + Tempo = 0; + Repet = 0; + tOnOff = 0; +} + + + +void Action::Arreter() { + int Tseconde = int(millis() / 1000); + if ((Tseconde - T_LastAction) >= Tempo || Idx == 0 || Actif != 1) { + if (Gpio > 0 || Idx == 0) { + digitalWrite(Gpio, OutOff); + T_LastAction = Tseconde; + } else { + if (On || ((Tseconde - T_LastAction) > Repet && Repet != 0)) { + CallExterne(Host, OrdreOff, Port); + T_LastAction = Tseconde; + } + } + On = false; + } +} +void Action::RelaisOn() { + int Tseconde = int(millis() / 1000); + if ((Tseconde - T_LastAction) >= Tempo) { + if (Gpio > 0) { + digitalWrite(Gpio, OutOn); + T_LastAction = Tseconde; + On = true; + } else { + if (Actif == 1) { + if (!On || ((Tseconde - T_LastAction) > Repet && Repet != 0)) { + CallExterne(Host, OrdreOn, Port); + T_LastAction = Tseconde; + } + On = true; + } + } + } +} +void Action::Prioritaire() { + int tempo_ = Tempo; + if (tOnOff != 0) { + Tempo = 0; + if (tOnOff > 0) { + RelaisOn(); + } else { + Arreter(); + } + Tempo = tempo_; + } +} + +void Action::Definir(String ligne) { + String RS = String((char)30); //Record Separator + Actif = byte(ligne.substring(0, ligne.indexOf(RS)).toInt()); + ligne = ligne.substring(ligne.indexOf(RS) + 1); + Titre = ligne.substring(0, ligne.indexOf(RS)); + ligne = ligne.substring(ligne.indexOf(RS) + 1); + Host = ligne.substring(0, ligne.indexOf(RS)); + ligne = ligne.substring(ligne.indexOf(RS) + 1); + Port = ligne.substring(0, ligne.indexOf(RS)).toInt(); + ligne = ligne.substring(ligne.indexOf(RS) + 1); + OrdreOn = ligne.substring(0, ligne.indexOf(RS)); + ligne = ligne.substring(ligne.indexOf(RS) + 1); + OrdreOff = ligne.substring(0, ligne.indexOf(RS)); + ligne = ligne.substring(ligne.indexOf(RS) + 1); + Repet = ligne.substring(0, ligne.indexOf(RS)).toInt(); + Repet = min(Repet, 32000); + Repet = max(0, Repet); + ligne = ligne.substring(ligne.indexOf(RS) + 1); + Tempo = ligne.substring(0, ligne.indexOf(RS)).toInt(); + Tempo = min(Tempo, 32000); + Tempo = max(0, Tempo); + if (Repet > 0) { + Repet = max(Tempo + 4, Repet); //Pour eviter conflit + } + ligne = ligne.substring(ligne.indexOf(RS) + 1); + Reactivite = byte(ligne.substring(0, ligne.indexOf(RS)).toInt()); + ligne = ligne.substring(ligne.indexOf(RS) + 1); + NbPeriode = byte(ligne.substring(0, ligne.indexOf(RS)).toInt()); + ligne = ligne.substring(ligne.indexOf(RS) + 1); + int Hdeb_ = 0; + for (byte i = 0; i < NbPeriode; i++) { + Type[i] = byte(ligne.substring(0, ligne.indexOf(RS)).toInt()); //NO,OFF,ON,PW,Triac + ligne = ligne.substring(ligne.indexOf(RS) + 1); + Hfin[i] = ligne.substring(0, ligne.indexOf(RS)).toInt(); + Hdeb[i] = Hdeb_; + Hdeb_ = Hfin[i]; + ligne = ligne.substring(ligne.indexOf(RS) + 1); + Vmin[i] = ligne.substring(0, ligne.indexOf(RS)).toInt(); + ligne = ligne.substring(ligne.indexOf(RS) + 1); + Vmax[i] = ligne.substring(0, ligne.indexOf(RS)).toInt(); + ligne = ligne.substring(ligne.indexOf(RS) + 1); + Tinf[i] = ligne.substring(0, ligne.indexOf(RS)).toInt(); + ligne = ligne.substring(ligne.indexOf(RS) + 1); + Tsup[i] = ligne.substring(0, ligne.indexOf(RS)).toInt(); + ligne = ligne.substring(ligne.indexOf(RS) + 1); + Tarif[i] = ligne.substring(0, ligne.indexOf(RS)).toInt(); + ligne = ligne.substring(ligne.indexOf(RS) + 1); + } +} +String Action::Lire() { + String GS = String((char)29); //Group Separator + String RS = String((char)30); //Record Separator + String S; + S += String(Actif) + RS; + S += Titre + RS; + S += Host + RS; + S += String(Port) + RS; + S += OrdreOn + RS; + S += OrdreOff + RS; + S += String(Repet) + RS; + S += String(Tempo) + RS; + S += String(Reactivite) + RS; + S += String(NbPeriode) + RS; + for (byte i = 0; i < NbPeriode; i++) { + S += String(Type[i]) + RS; + S += String(Hfin[i]) + RS; + S += String(Vmin[i]) + RS; + S += String(Vmax[i]) + RS; + S += String(Tinf[i]) + RS; + S += String(Tsup[i]) + RS; + S += String(Tarif[i]) + RS; + } + return S + GS; +} + + + + +byte Action::TypeEnCours(int Heure, float Temperature, int Ltarfbin) { //Retourne type d'action active à cette heure et test temperature OK + byte S = 1; + bool TemperatureOk; + bool TarifOk; + for (int i = 0; i < NbPeriode; i++) { + TemperatureOk = true; + if (Temperature > -100) { + if (Tinf[i] <= 1000 && Temperature * 10 > Tinf[i]) { TemperatureOk = false; } + if (Tsup[i] <= 1000 && Temperature * 10 < Tsup[i]) { TemperatureOk = false; } + } + TarifOk = true; + if (Ltarfbin > 0 && (Ltarfbin & Tarif[i]) == 0) TarifOk = false; + if (Heure >= Hdeb[i] && Heure <= Hfin[i] && TemperatureOk && TarifOk) S = Type[i]; + } + if (tOnOff > 0) S = 2; // Force On + if (tOnOff < 0) S = 1; // Force Off + return S; //0=NO,1=OFF,2=ON,3=PW,4=Triac +} +int Action::Valmin(int Heure) { //Retourne la valeur Vmin (ex seuil Triac) à cette heure + int S = 0; + for (int i = 0; i < NbPeriode; i++) { + if (Heure >= Hdeb[i] && Heure <= Hfin[i]) { + S = Vmin[i]; + } + } + return S; +} +int Action::Valmax(int Heure) { //Retourne la valeur Vmax (ex ouverture du Triac) à cette heure + int S = 0; + for (int i = 0; i < NbPeriode; i++) { + if (Heure >= Hdeb[i] && Heure <= Hfin[i]) { + S = Vmax[i]; + } + } + return S; +} + +void Action::InitGpio() { //Initialise les sorties GPIO pour des relais + int p; + String S; + String IS = String((char)31); //Input Separator + + if (Idx > 0) { + T_LastAction = 0; + Gpio = -1; + p = OrdreOn.indexOf(IS); + if (p >= 0) { + Gpio = OrdreOn.substring(0, p).toInt(); + OutOn = OrdreOn.substring(p+1).toInt(); + OutOff=(1+OutOn)%2; + if (Gpio > 0) { + pinMode(Gpio, OUTPUT); + digitalWrite(Gpio, OutOff); + } + } + } +} +void Action::CallExterne(String host, String url, int port) { + if (url != "") { + // Use WiFiClient class to create TCP connections + WiFiClient clientExt; + char hostbuf[host.length() + 1]; + host.toCharArray(hostbuf, host.length() + 1); + + if (!clientExt.connect(hostbuf, port)) { + StockMessage("connection to :" + host + " failed"); + return; + } + clientExt.print(String("GET ") + url + " HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Connection: close\r\n\r\n"); + unsigned long timeout = millis(); + while (clientExt.available() == 0) { + if (millis() - timeout > 5000) { + StockMessage(">>> clientESP_Ext Timeout ! : " + host); + clientExt.stop(); + return; + } + } + + // Read all the lines of the reply from server + while (clientExt.available()) { + String line = clientExt.readStringUntil('\r'); + } + } +} \ No newline at end of file diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/Actions.h b/docs/routers/F1ATB/Solar_Router_V10.00/Actions.h new file mode 100644 index 0000000..26d86f8 --- /dev/null +++ b/docs/routers/F1ATB/Solar_Router_V10.00/Actions.h @@ -0,0 +1,58 @@ +// ******************** +// Gestion des Actions +// ******************** +class Action { +private: + int Idx; //Index + void CallExterne(String host,String url, int port); + int T_LastAction=0; + int tempoTimer=0; + + + +public: + Action(); //Constructeur par defaut + Action(int aIdx); + + void Definir(String ligne); + String Lire(); + void Activer(float Pw, int Heure, float Temperature, int Ltarfbin); + void Arreter(); + void RelaisOn(); + void Prioritaire(); + + + byte TypeEnCours(int Heure,float Temperature, int Ltarfbin); + int Valmin(int Heure); + int Valmax(int Heure); + void InitGpio(); + byte Actif; + int Port; + int Repet; + int Tempo; + String Titre; + String Host; + String OrdreOn; + String OrdreOff; + int Gpio; + int OutOn; + int OutOff; + int tOnOff; + byte Reactivite; + byte NbPeriode; + bool On; + byte Type[8]; + int Hdeb[8]; + int Hfin[8]; + int Vmin[8]; + int Vmax[8]; + int Tinf[8]; + int Tsup[8]; + byte Tarif[8]; + +}; + + + extern void StockMessage(String m); + + diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/MQTT.ino b/docs/routers/F1ATB/Solar_Router_V10.00/MQTT.ino new file mode 100644 index 0000000..534e0d4 --- /dev/null +++ b/docs/routers/F1ATB/Solar_Router_V10.00/MQTT.ino @@ -0,0 +1,265 @@ +// ********************************************************************************************** +// * MQTT AUTO-DISCOVERY POUR HOME ASSISTANT ou DOMOTICZ * +// ********************************************************************************************** +bool Discovered = false; +char DEVICE[300]; +char ESP_ID[15]; +char mdl[30]; +char StateTopic[50]; + + +// Types de composants reconnus par HA et obligatoires pour l'Auto-Discovery. +const char *SSR = "sensor"; +const char *SLCT = "select"; +const char *NB = "number"; +const char *BINS = "binary_sensor"; +const char *SWTC = "switch"; +const char *TXT = "text"; +void GestionMQTT() { + if (MQTTRepet > 0 || Source_Temp == "tempMqtt" || Source == "Pmqtt" || subMQTT == 1) { + if (testMQTTconnected()) { + clientMQTT.loop(); + envoiVersMQTT(); + } + } +} + +bool testMQTTconnected() { + bool connecte = true; + if (!clientMQTT.connected()) { // si le mqtt n'est pas connecté (utile aussi lors de la 1ere connexion) + Serial.println("Connection au serveur MQTT ..."); + byte arr[4]; + arr[0] = MQTTIP & 0xFF; // 0x78 + arr[1] = (MQTTIP >> 8) & 0xFF; // 0x56 + arr[2] = (MQTTIP >> 16) & 0xFF; // 0x34 + arr[3] = (MQTTIP >> 24) & 0xFF; // 0x12 + String host = String(arr[3]) + "." + String(arr[2]) + "." + String(arr[1]) + "." + String(arr[0]); + clientMQTT.setServer(host.c_str(), MQTTPort); + clientMQTT.setCallback(callback); //Déclaration de la fonction de souscription + if (clientMQTT.connect(MQTTdeviceName.c_str(), MQTTUser.c_str(), MQTTPwd.c_str())) { // si l'utilisateur est connecté au mqtt + StockMessage(MQTTdeviceName + " connecté au broker MQTT"); + if (Source_Temp == "tempMqtt") { + char TopicV[50]; + sprintf(TopicV, "%s", TopicT.c_str()); + clientMQTT.subscribe(TopicV); + } + if (Source == "Pmqtt") { + char Topicp[50]; + sprintf(Topicp, "%s", TopicP.c_str()); + clientMQTT.subscribe(Topicp); + } + if (subMQTT == 1) { + for (int i = 0; i < NbActions; i++) { + if (LesActions[i].Actif > 0) { + char TopicAct[50]; + sprintf(TopicAct, "%s", LesActions[i].Titre.c_str()); + clientMQTT.subscribe(TopicAct); + } + } + } + sprintf(StateTopic, "%s/%s_state", MQTTPrefix.c_str(), MQTTdeviceName.c_str()); + byte mac[6]; // the MAC address of your Wifi shield + WiFi.macAddress(mac); + sprintf(ESP_ID, "%02x%02x%02x", mac[2], mac[1], mac[0]); // ID de l'entité pour HA + sprintf(mdl, "%s%s", "ESP32 - ", ESP_ID); // ID de l'entité pour HA + String mf = "F1ATB - https://f1atb.fr"; + String cu = "http://" + WiFi.localIP().toString(); + String hw = String(ESP.getChipModel()) + " rev." + String(ESP.getChipRevision()); + String sw = Version; + sprintf(DEVICE, "{\"ids\":\"%s\",\"name\":\"%s\",\"mdl\":\"%s\",\"mf\":\"%s\",\"hw\":\"%s\",\"sw\":\"%s\",\"cu\":\"%s\"}", ESP_ID, nomRouteur.c_str(), mdl, mf.c_str(), hw.c_str(), sw.c_str(), cu.c_str()); + + } else { // si utilisateur pas connecté au mqtt + StockMessage("Echec connexion MQTT : " + host); + connecte = false; + delay(100); + previousMQTTMillis=millis(); + } + } + return connecte; +} +void envoiVersMQTT() { + unsigned long tps = millis(); + int etat = 0; // utilisé pour l'envoie de l'état On/Off des actions. + if (int((tps - previousMQTTenvoiMillis) / 1000) > MQTTRepet && MQTTRepet != 0) { // Si Service MQTT activé avec période sup à 0 + previousMQTTenvoiMillis = tps; + if (!Discovered) { //(uniquement au démarrage discovery = 0) + sendMQTTDiscoveryMsg_global(); + } + SendDataToHomeAssistant(); // envoie du Payload au State topic + clientMQTT.loop(); + } +} +//Callback après souscription à un topic et réaliser une action +void callback(char *topic, byte *payload, unsigned int length) { + char Message[length + 1]; + for (int i = 0; i < length; i++) { + Message[i] = payload[i]; + } + Message[length] = '\0'; + String message = String(Message) + ","; + if (String(topic) == TopicT && Source_Temp == "tempMqtt") { //Temperature attendue + temperature = ValJson("temperature", message); + TemperatureValide = 5; + } + if (String(topic) == TopicP && Source == "Pmqtt") { //Mesure de puissance + PwMQTT = ValJson("Pw", message); + PvaMQTT = ValJson("Pva", message); + PfMQTT = ValJson("Pf", message); + P_MQTT_Brute = String(Message); + if (message.indexOf("Pw") > 0) LastPwMQTTMillis = millis(); + } + if (subMQTT == 1) { + for (int i = 0; i < NbActions; i++) { + if (LesActions[i].Actif > 0 && LesActions[i].Titre == String(topic)) { + LesActions[i].tOnOff=ValJson("tOnOff", message); + LesActions[i].Prioritaire(); + } + } + } + Serial.print(topic); + Serial.println(Message); +} +//************************************************************************* +//* CONFIG OF DISCOVERY MESSAGE FOR HOME ASSISTANT / DOMOTICZ * +//************************************************************************* + + +void sendMQTTDiscoveryMsg_global() { + String ActType; + // augmente la taille du buffer wifi Mqtt (voir PubSubClient.h) + clientMQTT.setBufferSize(700); // voir -->#define MQTT_MAX_PACKET_SIZE 256 is the default value in PubSubClient.h + if (Source == "UxIx2" || Source == "ShellyEm") { + DeviceToDiscover("PuissanceS_T", "W", "power", "0"); + DeviceToDiscover("PuissanceI_T", "W", "power", "0"); + DeviceToDiscover("Tension_T", "V", "voltage", "2"); + DeviceToDiscover("Intensite_T", "A", "current", "2"); + DeviceToDiscover("PowerFactor_T", "", "power_factor", "2"); + DeviceToDiscover("Energie_T_Soutiree", "Wh", "energy", "0"); + DeviceToDiscover("Energie_T_Injectee", "Wh", "energy", "0"); + DeviceToDiscover("EnergieJour_T_Soutiree", "Wh", "energy", "0"); + DeviceToDiscover("EnergieJour_T_Injectee", "Wh", "energy", "0"); + DeviceToDiscover("Frequence", "Hz", "frequency", "2"); + } + if (Source_Temp != "tempNo") DeviceToDiscover("Temperature", "°C", "temperature", "1"); + + + if (Source == "Linky") { + DeviceTextToDiscover("LTARF", "Option Tarifaire"); + DeviceToDiscover("Code_Tarifaire", "", "", "0"); + } + if (Source == "Enphase") { + DeviceToDiscover("PactProd", "W", "power", "0"); + DeviceToDiscover("PactConso_M", "W", "power", "0"); + } + + DeviceToDiscover("PuissanceS_M", "W", "power", "0"); + DeviceToDiscover("PuissanceI_M", "W", "power", "0"); + DeviceToDiscover("Tension_M", "V", "voltage", "2"); + DeviceToDiscover("Intensite_M", "A", "current", "2"); + DeviceToDiscover("PowerFactor_M", "", "power_factor", "2"); + DeviceToDiscover("Energie_M_Soutiree", "Wh", "energy", "0"); + DeviceToDiscover("Energie_M_Injectee", "Wh", "energy", "0"); + DeviceToDiscover("EnergieJour_M_Soutiree", "Wh", "energy", "0"); + DeviceToDiscover("EnergieJour_M_Injectee", "Wh", "energy", "0"); + + for (int i = 0; i < NbActions; i++) { + ActType = "Ouverture_Relais_" + String(i); + if (i == 0) ActType = "OuvertureTriac"; + DeviceToDiscover(ActType, "%", "power_factor", "0"); //Type power factor pour etre accepté par HA + } + + + Serial.println("Paramètres Auto-Discovery publiés !"); + + //clientMQTT.setBufferSize(512); // go to initial value wifi/mqtt buffer + Discovered = true; + + +} // END OF sendMQTTDiscoveryMsg_global + +void DeviceToDiscover(String VarName, String Unit, String Class, String Round) { + char value[700]; + char DiscoveryTopic[120]; + char UniqueID[50]; + char ValTpl[60]; + char state_class[60]; + String TitleName = String(MQTTdeviceName) + " " + String(VarName); + sprintf(DiscoveryTopic, "%s/%s/%s_%s/%s", MQTTPrefix.c_str(), SSR, MQTTdeviceName.c_str(), VarName.c_str(), "config"); + sprintf(UniqueID, "%s_%s", MQTTdeviceName.c_str(), VarName.c_str()); + sprintf(ValTpl, "{{ value_json.%s|default(0)|round(%s)}}", VarName.c_str(), Round.c_str()); + sprintf(state_class, "%s", ""); + if (Unit == "Wh" || Unit == "kWh") { + sprintf(state_class, "\"state_class\":\"total_increasing\"%s,", state_class); + } + sprintf(value, "{\"name\": \"%s\",\"uniq_id\": \"%s\",\"stat_t\": \"%s\",\"device_class\": \"%s\",\"unit_of_meas\": \"%s\",%s\"val_tpl\": \"%s\",\"device\": %s}", TitleName.c_str(), UniqueID, StateTopic, Class.c_str(), Unit.c_str(), state_class, ValTpl, DEVICE); + clientMQTT.publish(DiscoveryTopic, value); +} +void DeviceBinToDiscover(String VarName, String TitleName) { + char value[700]; + char DiscoveryTopic[120]; + char UniqueID[50]; + char ValTpl[60]; + String init = "OFF"; // default value + String ic = "mdi:electric-switch"; + sprintf(DiscoveryTopic, "%s/%s/%s_%s/%s", MQTTPrefix.c_str(), BINS, MQTTdeviceName.c_str(), VarName.c_str(), "config"); + sprintf(UniqueID, "%s_%s", MQTTdeviceName.c_str(), VarName.c_str()); + sprintf(ValTpl, "{{ value_json.%s}}", VarName.c_str()); + sprintf(value, "{\"name\": \"%s\",\"uniq_id\": \"%s\",\"stat_t\": \"%s\",\"init\": \"%s\",\"ic\": \"%s\",\"val_tpl\": \"%s\",\"device\": %s}", TitleName.c_str(), UniqueID, StateTopic, init.c_str(), ic.c_str(), ValTpl, DEVICE); + clientMQTT.publish(DiscoveryTopic, value); +} + +void DeviceTextToDiscover(String VarName, String TitleName) { + char value[600]; + char DiscoveryTopic[120]; + char UniqueID[50]; + char ValTpl[50]; + sprintf(DiscoveryTopic, "%s/%s/%s_%s/%s", MQTTPrefix.c_str(), SSR, MQTTdeviceName.c_str(), VarName.c_str(), "config"); + sprintf(UniqueID, "%s_%s", MQTTdeviceName.c_str(), VarName.c_str()); + sprintf(ValTpl, "{{ value_json.%s }}", VarName.c_str()); + sprintf(value, "{\"name\": \"%s\",\"uniq_id\": \"%s\",\"stat_t\": \"%s\",\"device_class\": \"%s\",\"val_tpl\": \"%s\",\"device\": %s}", TitleName.c_str(), UniqueID, StateTopic, "enum", ValTpl, DEVICE); + clientMQTT.publish(DiscoveryTopic, value); +} +//**************************************** +//* ENVOIE DES DATAS VERS HOME ASSISTANT * +//**************************************** + +void SendDataToHomeAssistant() { + String ActType; + char value[1000]; + sprintf(value, "{\"PuissanceS_M\": %d, \"PuissanceI_M\": %d, \"Tension_M\": %.1f, \"Intensite_M\": %.1f, \"PowerFactor_M\": %.2f, \"Energie_M_Soutiree\":%d,\"Energie_M_Injectee\":%d, \"EnergieJour_M_Soutiree\":%d, \"EnergieJour_M_Injectee\":%d", PuissanceS_M, PuissanceI_M, Tension_M, Intensite_M, PowerFactor_M, Energie_M_Soutiree, Energie_M_Injectee, EnergieJour_M_Soutiree, EnergieJour_M_Injectee); + + if (Source == "UxIx2" || Source == "ShellyEm") { + sprintf(value, "%s,\"PuissanceS_T\": %d, \"PuissanceI_T\": %d, \"Tension_T\": %.1f, \"Intensite_T\": %.1f, \"PowerFactor_T\": %.2f, \"Energie_T_Soutiree\":%d,\"Energie_T_Injectee\":%d, \"EnergieJour_T_Soutiree\":%d, \"EnergieJour_T_Injectee\":%d, \"Frequence\":%.2f", value, PuissanceS_T, PuissanceI_T, Tension_T, Intensite_T, PowerFactor_T, Energie_T_Soutiree, Energie_T_Injectee, EnergieJour_T_Soutiree, EnergieJour_T_Injectee, Frequence); + } + if (temperature > -100 && Source_Temp != "tempNo") { + sprintf(value, "%s,\"Temperature\": %.1f", value, temperature); + } + if (Source == "Linky") { + int code = 0; + if (LTARF.indexOf("HEURE CREUSE") >= 0) code = 1; //Code Linky + if (LTARF.indexOf("HEURE PLEINE") >= 0) code = 2; + if (LTARF.indexOf("HC BLEU") >= 0) code = 11; + if (LTARF.indexOf("HP BLEU") >= 0) code = 12; + if (LTARF.indexOf("HC BLANC") >= 0) code = 13; + if (LTARF.indexOf("HP BLANC") >= 0) code = 14; + if (LTARF.indexOf("HC ROUGE") >= 0) code = 15; + if (LTARF.indexOf("HP ROUGE") >= 0) code = 16; + if (LTARF.indexOf("TEMPO_BLEU") >= 0) code = 17; // Code EDF + if (LTARF.indexOf("TEMPO_BLANC") >= 0) code = 18; + if (LTARF.indexOf("TEMPO_ROUGE") >= 0) code = 19; + sprintf(value, "%s,\"LTARF\":\"%s\", \"Code_Tarifaire\":%d", value, LTARF, code); + } + + if (Source == "Enphase") { + sprintf(value, "%s,\"PactProd\":%d, \"PactConso_M\":%d", value, PactProd, PactConso_M); + } + + for (int i = 0; i < NbActions; i++) { + ActType = "Ouverture_Relais_" + String(i); + if (i == 0) ActType = "OuvertureTriac"; + int Ouv = 100 - Retard[i]; + sprintf(value, "%s,\"%s\":%d", value, ActType.c_str(), Ouv); + } + sprintf(value, "%s}", value); + bool published = clientMQTT.publish(StateTopic, value); +} diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/Server.ino b/docs/routers/F1ATB/Solar_Router_V10.00/Server.ino new file mode 100644 index 0000000..a57322c --- /dev/null +++ b/docs/routers/F1ATB/Solar_Router_V10.00/Server.ino @@ -0,0 +1,540 @@ +// *************** +// * WEB SERVER * +// *************** +void Init_Server() { + //Init Web Server on port 80 + server.on("/", handleRoot); + server.on("/MainJS", handleMainJS); + server.on("/Para", handlePara); + server.on("/ParaJS", handleParaJS); + server.on("/ParaRouteurJS", handleParaRouteurJS); + server.on("/ParaAjax", handleParaAjax); + server.on("/ParaRouteurAjax", handleParaRouteurAjax); + server.on("/ParaUpdate", handleParaUpdate); + server.on("/Actions", handleActions); + server.on("/ActionsJS", handleActionsJS); + server.on("/ActionsJS2", handleActionsJS2); + server.on("/ActionsUpdate", handleActionsUpdate); + server.on("/ActionsAjax", handleActionsAjax); + server.on("/Brute", handleBrute); + server.on("/BruteJS", handleBruteJS); + server.on("/ajax_histo48h", handleAjaxHisto48h); + server.on("/ajax_histo1an", handleAjaxHisto1an); + server.on("/ajax_dataRMS", handleAjaxRMS); + server.on("/ajax_dataESP32", handleAjaxESP32); + server.on("/ajax_data", handleAjaxData); + server.on("/ajax_data10mn", handleAjaxData10mn); + server.on("/ajax_etatActions", handleAjax_etatActions); + server.on("/ajax_Temperature", handleAjaxTemperature); + server.on("/SetGPIO", handleSetGpio); + server.on("/restart", handleRestart); + server.on("/Change_Wifi", handleChange_Wifi); + server.on("/AP_ScanWifi", handleAP_ScanWifi); + server.on("/AP_SetWifi", handleAP_SetWifi); + server.onNotFound(handleNotFound); + + //SERVER OTA + server.on("/OTA", HTTP_GET, []() { + server.sendHeader("Connection", "close"); + server.send(200, "text/html", OtaHtml); + }); + /*handling uploading firmware file */ + server.on( + "/update", HTTP_POST, []() { + server.sendHeader("Connection", "close"); + server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK"); + ESP.restart(); + }, + []() { + HTTPUpload& upload = server.upload(); + if (upload.status == UPLOAD_FILE_START) { + Serial.printf("Update: %s\n", upload.filename.c_str()); + if (!Update.begin(UPDATE_SIZE_UNKNOWN)) { //start with max available size + Update.printError(Serial); + } + } else if (upload.status == UPLOAD_FILE_WRITE) { + /* flashing firmware to ESP*/ + if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) { + Update.printError(Serial); + } + } else if (upload.status == UPLOAD_FILE_END) { + if (Update.end(true)) { //true to set the size to the current progress + Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize); + } else { + Update.printError(Serial); + } + } + }); + + + server.begin(); +} + + +void handleRoot() { //Pages principales + if (WiFi.getMode() != WIFI_STA) { // en AP et STA mode + server.send(200, "text/html", String(ConnectAP_Html)); + } else { //Station Mode seul + server.send(200, "text/html", String(MainHtml)); + } +} +void handleChange_Wifi(){ + server.send(200, "text/html", String(ConnectAP_Html)); +} +void handleMainJS() { //Code Javascript + server.send(200, "text/html", String(MainJS)); // Javascript code +} +void handleBrute() { //Page données brutes + server.send(200, "text/html", String(PageBrute)); +} +void handleBruteJS() { //Code Javascript + server.send(200, "text/html", String(PageBruteJS)); // Javascript code +} + +void handleAjaxRMS() { // Envoi des dernières données brutes reçues du RMS + String S = ""; + String RMSExtDataB = ""; + int LastIdx = server.arg(0).toInt(); + if (Source == "Ext") { + // Use WiFiClient class to create TCP connections + WiFiClient clientESP_RMS; + byte arr[4]; + arr[0] = RMSextIP & 0xFF; // 0x78 + arr[1] = (RMSextIP >> 8) & 0xFF; // 0x56 + arr[2] = (RMSextIP >> 16) & 0xFF; // 0x34 + arr[3] = (RMSextIP >> 24) & 0xFF; // 0x12 + + String host = String(arr[3]) + "." + String(arr[2]) + "." + String(arr[1]) + "." + String(arr[0]); + if (!clientESP_RMS.connect(host.c_str(), 80)) { + StockMessage("connection to ESP_RMS external failed (call from handleAjaxRMS)"); + return; + } + String url = "/ajax_dataRMS?idx=" + String(LastIdx); + clientESP_RMS.print(String("GET ") + url + " HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Connection: close\r\n\r\n"); + unsigned long timeout = millis(); + while (clientESP_RMS.available() == 0) { + if (millis() - timeout > 5000) { + StockMessage(">>> clientESP_RMS Timeout !"); + clientESP_RMS.stop(); + return; + } + } + // Lecture des données brutes distantes + while (clientESP_RMS.available()) { + RMSExtDataB += clientESP_RMS.readStringUntil('\r'); + } + S = RMSExtDataB.substring(RMSExtDataB.indexOf("\n\n") + 2); + } else { + S = DATE + RS + Source_data; + if (Source_data == "UxI") { + S += RS + String(Tension_M) + RS + String(Intensite_M) + RS + String(PowerFactor_M) + GS; + int i0 = 0; + int i1 = 0; + for (int i = 0; i < 100; i++) { + i1 = (i + 1) % 100; + if (voltM[i] <= 0 && voltM[i1] > 0) { + i0 = i1; //Point de départ tableau . Phase positive + i = 100; + } + } + for (int i = 0; i < 100; i++) { + i1 = (i + i0) % 100; + S += String(int(10 * voltM[i1])) + RS; //Voltages*10. Increase dynamic + } + S += "0" + GS; + for (int i = 0; i < 100; i++) { + i1 = (i + i0) % 100; + S += String(int(10 * ampM[i1])) + RS; //Currents*10 + } + S += "0"; + } + if (Source_data == "UxIx2") { + + S += GS + String(Tension_M) + RS + String(Intensite_M) + RS + String(PuissanceS_M - PuissanceI_M) + RS + String(PowerFactor_M) + RS + String(Energie_M_Soutiree) + RS + String(Energie_M_Injectee); + S += RS + String(Tension_T) + RS + String(Intensite_T) + RS + String(PuissanceS_T - PuissanceI_T) + RS + String(PowerFactor_T) + RS + String(Energie_T_Soutiree) + RS + String(Energie_T_Injectee); + S += RS + String(Frequence); + } + if (Source_data == "Linky") { + S += GS; + while (LastIdx != IdxDataRawLinky) { + S += String(DataRawLinky[LastIdx]); + LastIdx = (1 + LastIdx) % 10000; + } + S += GS + String(IdxDataRawLinky); + } + if (Source_data == "Enphase") { + S += GS + String(Tension_M) + RS + String(Intensite_M) + RS + String(PuissanceS_M - PuissanceI_M) + RS + String(PowerFactor_M) + RS + String(Energie_M_Soutiree) + RS + String(Energie_M_Injectee); + S += RS + String(PactProd) + RS + String(PactConso_M); + String SessionId = "Not Received from Enphase"; + if (Session_id != "") { + SessionId = "Ok Received from Enphase"; + } + String Token_Enphase = "Not Received from Enphase"; + if (TokenEnphase.length() > 50) { + Token_Enphase = "Ok Received from Enphase"; + } + if (EnphaseUser == "") { + SessionId = "Not Requested"; + Token_Enphase = "Not Requested"; + } + S += RS + SessionId; + + S += RS + Token_Enphase; + } + if (Source_data == "SmartG") { + S += GS + SG_dataBrute; + } + if (Source_data == "ShellyEm") { + S += GS + ShEm_dataBrute; + } + if (Source_data == "UxIx3") { + S += GS + MK333_dataBrute; + } + if (Source_data == "Pmqtt") { + S += GS + P_MQTT_Brute; + } + } + + server.send(200, "text/html", S); +} +void handleAjaxHisto48h() { // Envoi Historique de 50h (600points) toutes les 5mn + String S = ""; + String T = ""; + String U = ""; + String Ouverture = ""; + int iS = IdxStockPW; + for (int i = 0; i < 600; i++) { + S += String(tabPw_Maison_5mn[iS]) + ","; + T += String(tabPw_Triac_5mn[iS]) + ","; + U += String(float(tabTemperature_5mn[iS]) * 0.1) + ","; + iS = (1 + iS) % 600; + } + for (int i = 0; i < NbActions; i++) { + if ((LesActions[i].Actif > 0) && (ITmode > 0 || i > 0)) { + iS = IdxStockPW; + if (LesActions[i].Actif > 0) { + Ouverture += GS; + for (int j = 0; j < 600; j++) { + Ouverture += String(tab_histo_ouverture[i][iS]) + RS; + iS = (1 + iS) % 600; + } + Ouverture += LesActions[i].Titre; + } + } + } + server.send(200, "text/html", Source_data + GS + S + GS + T + GS + String(temperature) + GS + U + Ouverture); +} +void handleAjaxESP32() { // Envoi des dernières infos sur l'ESP32 + IT10ms = 0; + IT10ms_in = 0; + String S = ""; + float H = float(T_On_seconde) / 3600; + String coeur0 = String(int(previousTimeRMSMin)) + ", " + String(int(previousTimeRMSMoy)) + ", " + String(int(previousTimeRMSMax)); + String coeur1 = String(int(previousLoopMin)) + ", " + String(int(previousLoopMoy)) + ", " + String(int(previousLoopMax)); + S += String(H) + RS + WiFi.RSSI() + RS + WiFi.BSSIDstr() + RS + WiFi.macAddress() + RS + ssid + RS + WiFi.localIP().toString() + RS + WiFi.gatewayIP().toString() + RS + WiFi.subnetMask().toString(); + S += RS + coeur0 + RS + coeur1 + RS + String(P_cent_EEPROM) + RS; + delay(15); //Comptage interruptions + if (IT10ms_in > 0) { + S += String(IT10ms_in) + "/" + String(IT10ms); + } else { + S += "Pas de Triac"; + } + if (ITmode > 0) { + S += RS + "Secteur"; + } else { + S += RS + "Horloge ESP"; + } + int j = idxMessage; + for (int i = 0; i < 10; i++) { + S += RS + MessageH[j]; + j = (j + 1) % 10; + } + server.send(200, "text/html", S); +} +void handleAjaxHisto1an() { // Envoi Historique Energie quotiiienne sur 1 an 370 points + server.send(200, "text/html", HistoriqueEnergie1An()); +} +void handleAjaxData() { //Données page d'accueil + String DateLast = "Attente de l'heure par Internet"; + if (DATEvalid) { + DateLast = DATE; + } + String S = "Deb" + RS + DateLast + RS + Source_data + RS + LTARF + RS + STGE + RS + String(temperature) + RS + String(Pva_valide); + S += GS + String(PuissanceS_M) + RS + String(PuissanceI_M) + RS + String(PVAS_M) + RS + String(PVAI_M); + S += RS + String(EnergieJour_M_Soutiree) + RS + String(EnergieJour_M_Injectee) + RS + String(Energie_M_Soutiree) + RS + String(Energie_M_Injectee); + if (Source_data == "UxIx2" || (Source_data == "ShellyEm" && EnphaseSerial.toInt() < 3)) { //UxIx2 ou Shelly monophasé avec 2 sondes + S += GS + String(PuissanceS_T) + RS + String(PuissanceI_T) + RS + String(PVAS_T) + RS + String(PVAI_T); + S += RS + String(EnergieJour_T_Soutiree) + RS + String(EnergieJour_T_Injectee) + RS + String(Energie_T_Soutiree) + RS + String(Energie_T_Injectee); + } + S += GS + "Fin"; + server.send(200, "text/html", S); +} +void handleAjax_etatActions() { + int Force = server.arg("Force").toInt(); + int NumAction = server.arg("NumAction").toInt(); + if (Force != 0) { + if (Force > 0) { + if (LesActions[NumAction].tOnOff < 0) { + LesActions[NumAction].tOnOff = 0; + } else { + LesActions[NumAction].tOnOff += 30; + } + } else { + if (LesActions[NumAction].tOnOff > 0) { + LesActions[NumAction].tOnOff = 0; + } else { + LesActions[NumAction].tOnOff -= 30; + } + } + LesActions[NumAction].Prioritaire(); + } + int NbActifs = 0; + String S = ""; + String On_; + for (int i = 0; i < NbActions; i++) { + if ((LesActions[i].Actif > 0) && (ITmode > 0 || i > 0)) { //Pas de Triac en synchro horloge interne + S += String(i) + RS + LesActions[i].Titre + RS; + if (LesActions[i].Actif == 1 && i > 0) { + if (LesActions[i].On) { + S += "On" + RS; + } else { + S += "Off" + RS; + } + } else { + S += String(100 - Retard[i]) + RS; + } + S += String(LesActions[i].tOnOff) + RS; + S += GS; + NbActifs++; + } + } + S = String(temperature) + GS + String(Source_data) + GS + String(RMSextIP) + GS + NbActifs + GS + S; + server.send(200, "text/html", S); +} +void handleAjaxTemperature() { + server.send(200, "text/html", GS + String(temperature) + RS); +} +void handleRestart() { // Eventuellement Reseter l'ESP32 à distance + server.send(200, "text/plain", "OK Reset. Attendez."); + delay(1000); + ESP.restart(); +} +void handleAjaxData10mn() { // Envoi Historique de 10mn (300points)Energie Active Soutiré - Injecté + String S = ""; + String T = ""; + int iS = IdxStock2s; + for (int i = 0; i < 300; i++) { + S += String(tabPw_Maison_2s[iS]) + ","; + S += String(tabPva_Maison_2s[iS]) + ","; + T += String(tabPw_Triac_2s[iS]) + ","; + T += String(tabPva_Triac_2s[iS]) + ","; + iS = (1 + iS) % 300; + } + server.send(200, "text/html", Source_data + GS + S + GS + T); +} +void handleActions() { + server.send(200, "text/html", String(ActionsHtml)); +} +void handleActionsJS() { + server.send(200, "text/html", String(ActionsJS)); +} +void handleActionsJS2() { + server.send(200, "text/html", String(ActionsJS2)); +} +void handleActionsUpdate() { + int adresse_max = 0; + String s = server.arg("actions"); + String ligne = ""; + InitGpioActions(); //RAZ anciennes actions + NbActions = 0; + while (s.indexOf(GS) > 3 && NbActions < LesActionsLength) { + ligne = s.substring(0, s.indexOf(GS)); + s = s.substring(s.indexOf(GS) + 1); + LesActions[NbActions].Definir(ligne); + NbActions = NbActions + 1; + } + adresse_max = EcritureEnROM(); + server.send(200, "text/plain", "OK" + String(adresse_max)); + InitGpioActions(); +} +void handleActionsAjax() { + String S = String(temperature) + RS + String(LTARFbin) + RS + String(pTriac) + GS; + for (int i = 0; i < NbActions; i++) { + S += LesActions[i].Lire(); + } + server.send(200, "text/html", S); +} + +void handlePara() { + server.send(200, "text/html", String(ParaHtml)); +} +void handleParaUpdate() { + String Vp[32]; + String lesparas = server.arg("lesparas") + RS; + int idx = 0; + while (lesparas.length() > 0) { + Vp[idx] = lesparas.substring(0, lesparas.indexOf(RS)); + lesparas = lesparas.substring(lesparas.indexOf(RS) + 1); + idx++; + Vp[idx].trim(); + } + dhcpOn = byte(Vp[0].toInt()); + IP_Fixe = strtoul(Vp[1].c_str(), NULL, 10); + Gateway = strtoul(Vp[2].c_str(), NULL, 10); + masque = strtoul(Vp[3].c_str(), NULL, 10); + dns = strtoul(Vp[4].c_str(), NULL, 10); + Source = Vp[5]; + RMSextIP = strtoul(Vp[6].c_str(), NULL, 10); + EnphaseUser = Vp[7]; + EnphasePwd = Vp[8]; + EnphaseSerial = Vp[9]; + TopicP = Vp[10]; + MQTTRepet = Vp[11].toInt(); + MQTTIP = strtoul(Vp[12].c_str(), NULL, 10); + MQTTPort = Vp[13].toInt(); //2 bytes + MQTTUser = Vp[14]; + MQTTPwd = Vp[15]; + MQTTPrefix = Vp[16]; + MQTTdeviceName = Vp[17]; + subMQTT = byte(Vp[18].toInt()); + nomRouteur = Vp[19]; + nomSondeFixe = Vp[20]; + nomSondeMobile = Vp[21]; + nomTemperature = Vp[22]; + Source_Temp = Vp[23]; + TopicT = Vp[24]; + IPtemp = strtoul(Vp[25].c_str(), NULL, 10); + CalibU = Vp[26].toInt(); //2 bytes + CalibI = Vp[27].toInt(); //2 bytes + TempoEDFon = byte(Vp[28].toInt()); + WifiSleep = byte(Vp[29].toInt()); + pSerial = byte(Vp[30].toInt()); + pTriac = byte(Vp[31].toInt()); + int adresse_max = EcritureEnROM(); + if (Source != "Ext") { + Source_data = Source; + } + server.send(200, "text/plain", "OK" + String(adresse_max)); + LastHeureEDF = -1; +} +void handleParaJS() { + server.send(200, "text/html", String(ParaJS)); +} +void handleParaRouteurJS() { + server.send(200, "text/html", String(ParaRouteurJS)); +} +void handleParaAjax() { + String S = String(dhcpOn) + RS + String(IP_Fixe) + RS + String(Gateway) + RS + String(masque) + RS + String(dns) + RS + Source + RS + String(RMSextIP) + RS; + S += EnphaseUser + RS + EnphasePwd + RS + EnphaseSerial + RS + TopicP; + S += RS + String(MQTTRepet) + RS + String(MQTTIP) + RS + String(MQTTPort) + RS + MQTTUser + RS + MQTTPwd; + S += RS + MQTTPrefix + RS + MQTTdeviceName + RS + String(subMQTT) + RS + nomRouteur + RS + nomSondeFixe + RS + nomSondeMobile; + S += RS + String(temperature) + RS + nomTemperature + RS + Source_Temp + RS + TopicT + RS + String(IPtemp); + S += RS + String(CalibU) + RS + String(CalibI); + S += RS + String(TempoEDFon) + RS + String(WifiSleep) + RS + String(pSerial) + RS + String(pTriac); + server.send(200, "text/html", S); +} +void handleParaRouteurAjax() { + String S = Source + RS + Source_data + RS + nomRouteur + RS + Version + RS + nomSondeFixe + RS + nomSondeMobile + RS + String(RMSextIP); + S += RS + nomTemperature; + server.send(200, "text/html", S); +} +void handleSetGpio() { + int gpio = server.arg("gpio").toInt(); + int out = server.arg("out").toInt(); + String S = "Refut : gpio =" + String(gpio) + " out =" + String(out); + if (gpio >= 0 && gpio <= 33 && out >= 0 && out <= 1) { + pinMode(gpio, OUTPUT); + digitalWrite(gpio, out); + S = "OK : gpio =" + String(gpio) + " out =" + String(out); + } + server.send(200, "text/html", S); +} +void handleAP_ScanWifi() { + server.send(200, "text/html", Liste_AP); +} +void Liste_WIFI() { //Doit être fait avant toute connection WIFI depuis biblio ESP32 3.0.1 + WIFIbug = 0; + ComBug=0; + int n = 0; + WiFi.disconnect(); + delay(100); + Serial.println("Scan start"); + // WiFi.scanNetworks will return the number of networks found. + n = WiFi.scanNetworks(); + Serial.println("Scan done"); + Liste_AP = ""; + if (n == 0) { + Serial.println("Pas de réseau Wifi trouvé"); + } else { + Serial.print(n); + Serial.println(" réseaux trouvés"); + Serial.println("Nr | SSID | RSSI | CH | Encryption"); + for (int i = 0; i < n; ++i) { + // Print SSID and RSSI for each network found + Serial.printf("%2d", i + 1); + Serial.print(" | "); + Serial.printf("%-32.32s", WiFi.SSID(i).c_str()); + Serial.print(" | "); + Serial.printf("%4d", WiFi.RSSI(i)); + Serial.println(); + Liste_AP += WiFi.SSID(i).c_str() + RS + WiFi.RSSI(i) + GS; + } + } + WiFi.scanDelete(); +} +void handleAP_SetWifi() { + WIFIbug = 0; + ComBug =0; + Serial.println("Set Wifi"); + String NewSsid = server.arg("ssid"); + NewSsid.trim(); + String NewPassword = server.arg("passe"); + NewPassword.trim(); + Serial.println(NewSsid); + Serial.println(NewPassword); + ssid = NewSsid; + password = NewPassword; + StockMessage("Wifi Begin : " + ssid); + WiFi.begin(ssid.c_str(), password.c_str()); + unsigned long newstartMillis = millis(); + while (WiFi.status() != WL_CONNECTED && (millis() - newstartMillis < 20000)) { // Attente connexion au Wifi + Serial.write('!'); + Gestion_LEDs(); + Serial.print(WiFi.status()); + delay(300); + } + Serial.println(); + String S = ""; + if (WiFi.status() == WL_CONNECTED) { + Serial.print("IP address: "); + Serial.println(WiFi.localIP()); + String IP = WiFi.localIP().toString(); + S = "Ok" + RS; + S += "ESP 32 connecté avec succès au wifi : " + ssid + " avec l'adresse IP : " + IP; + S += "

Connectez vous au wifi : " + ssid; + S += "

Cliquez sur l'adresse : http://" + IP + ""; + dhcpOn = 1; + EcritureEnROM(); + } else { + S = "No" + RS + "ESP32 non connecté à :" + ssid + "
"; + } + server.send(200, "text/html", S); + delay(1000); + ESP.restart(); +} + + +void handleNotFound() { //Page Web pas trouvé + String message = "Fichier non trouvé\n\n"; + message += "URI: "; + message += server.uri(); + message += "\nMethod: "; + message += (server.method() == HTTP_GET) ? "GET" : "POST"; + message += "\nArguments: "; + message += server.args(); + message += "\n"; + for (uint8_t i = 0; i < server.args(); i++) { + message += " " + server.argName(i) + ": " + server.arg(i) + "\n"; + } + server.send(404, "text/plain", message); +} \ No newline at end of file diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/Solar_Router_V10.00.ino b/docs/routers/F1ATB/Solar_Router_V10.00/Solar_Router_V10.00.ino new file mode 100644 index 0000000..26b9c55 --- /dev/null +++ b/docs/routers/F1ATB/Solar_Router_V10.00/Solar_Router_V10.00.ino @@ -0,0 +1,1124 @@ +/* + PV Router / Routeur Solaire + **************************************** + Version V10.00 + + RMS=Routeur Multi Sources + + Choix de 9 sources différentes pour lire la consommation électrique en entrée de maison + - lecture de la tension avec un transformateur et du courant avec une sonde ampèremétrique (UxI) + - lecture des données du Linky (Linky) + - module (JSY-MK-194T) intégrant une mesure de tension secteur et 2 sondes ampèmétriques (UxIx2) + - module (JSY-MK-333) pour une installation triphasé + - Lecture passerelle Enphase - Envoy-S metered (firmware V5 et V7) + - Lecture depuis un autre ESP qui comprend l'une des 4 sources citées plus haut + - Lecture avec Shelly Em + - Lecture compteur SmartG + - Lecture via MQTT + + En option une mesure de température en interne (DS18B20), en externe ou via MQTT est possible. + + Historique des versions + - V9.00_RMS + Stockage des températures avec une décimale + Simplification changement de nom de réseau WIFI + Choix mode Wifi avec ou sans veille + Sélection source de température + Source de puissance reçue via MQTT + Souscription MQTT à une température externe + Souscription MQTT pour forcer On ou Off les actionneurs. + - V9.01_RMS fonctionne avec la bibliothèque ESP32 Version 2.0.17 + Validation Pva_valide pour les Linky en CACSI + - V9.02_RMS fonctionne avec la bibliothèque ES¨P32 V 3.01 . + Suite au passage de la bibliothèque ESP32 en Version 3.01 importants changement pour le routeur sur le WIFI, les Timers, Le Watchdog et la partition mémoire FLASH. + Attention à ne pas utiliser la bibliothèque ESP32 en Version 3.00, elle est bugée et génère 20% de plus de code. + Filtrage des températures pour tolérer une perte éventuelle de mesure + - V9.03_RMS + Suite au changement de bibliothèque ESP32 en V3.0.1, le scan réseau pour un changement de nom de WIFI ne fonctionnait plus. Scan fait maintenant au boot. + - V10.00 + OTA par le Web directement en complément de l'Arduino IDE + Modification des calculs de puissance en UxIx3 pour avoir une représentation similaire au Linky (Merci PhDV61) + Modification de la surveillance Watchdog + + + Les détails sont disponibles sur / Details are available here: + https://f1atb.fr Section Domotique / Home Automation + + F1ATB Juin 2024 + + GNU Affero General Public License (AGPL) / AGPL-3.0-or-later + + + +*/ +#define Version "10.00" +#define HOSTNAME "RMS-ESP32-" +#define CLE_Rom_Init 912567899 //Valeur pour tester si ROM vierge ou pas. Un changement de valeur remet à zéro toutes les données. / Value to test whether blank ROM or not. + + +//Librairies +#include +#include +#include +#include +#include //Modification On The Air +#include //Pour un Watchdog +#include //Librairie pour la gestion Mqtt +#include "EEPROM.h" //Librairie pour le stockage en EEPROM historique quotidien +#include "esp_sntp.h" +#include "OneWire.h" +#include "DallasTemperature.h" +#include "UrlEncode.h" +#include +#include + +//Program routines +#include "pageHtmlBrute.h" +#include "pageHtmlMain.h" +#include "pageHtmlConnect.h" +#include "pageHtmlPara.h" +#include "pageHtmlActions.h" +#include "pageHtmlOTA.h" +#include "Actions.h" + + +//Watchdog de 180 secondes. Le systeme se Reset si pas de dialoque avec le LINKY ou JSY-MK-194T/333 ou Enphase-Envoy pendant 180s +//Watchdog for 180 seconds. The system resets if no dialogue with the Linky or JSY-MK-194T/333 or Enphase-Envoy for 180s +#define WDT_TIMEOUT 180 + +//PINS - GPIO + +#define AnalogIn0 35 //Pour Routeur Uxi +#define AnalogIn1 32 +#define AnalogIn2 33 //Note: si GPIO 33 non disponible sur la carte ESP32, utilisez la 34. If GPIO 33 not available on the board replace by GPIO 34 +#define RXD2_1 16 //Pour Routeur Linky ou UxIx2 (sur carte ESP32 simple): Couple RXD2=26 et TXD2=27 . Pour carte ESP32 4 relais : Couple RXD2=17 et TXD2=27 +#define TXD2_1 17 +#define RXD2_2 26 //Pour Routeur Linky ou UxIx2 (sur carte ESP32 simple): Couple RXD2=26 et TXD2=27 . Pour carte ESP32 4 relais : Couple RXD2=17 et TXD2=27 +#define TXD2_2 27 +#define SER_BUF_SIZE 4096 +#define LedYellow 18 +#define LedGreen 19 +#define pulseTriac_1 4 +#define zeroCross_1 5 +#define pulseTriac_2 22 +#define zeroCross_2 23 +#define pinTemp 13 //Capteur température + + +//Nombre Actions Max +#define LesActionsLength 10 +//VARIABLES +const char *ap_default_ssid; // Mode Access point IP: 192.168.4.1 +const char *ap_default_psk = NULL; // Pas de mot de passe en AP, + +//Paramètres pour le stockage en ROM apres les données du RMS +unsigned long Cle_ROM; + +String ssid = ""; +String password = ""; +String Source = "UxI"; +String Source_data = "UxI"; +byte dhcpOn = 1; +unsigned long IP_Fixe = 0; +unsigned long Gateway = 0; +unsigned long masque = 4294967040; +unsigned long dns = 0; +unsigned long RMSextIP = 0; +unsigned int MQTTRepet = 0; +unsigned long MQTTIP = 0; +unsigned int MQTTPort = 1883; +String MQTTUser = "User"; +String MQTTPwd = "password"; +String MQTTPrefix = "homeassistant"; // prefix obligatoire pour l'auto-discovery entre HA et Core-Mosquitto (par défaut c'est homeassistant) +String MQTTdeviceName = "routeur_rms"; +String TopicP = "PuissanceMaison"; +String TopicT = "TemperatureMQTT"; +unsigned long IPtemp = 0; +byte subMQTT = 0; +String nomRouteur = "Routeur - RMS"; +String nomSondeFixe = "Données seconde sonde"; +String nomSondeMobile = "Données Maison"; +String nomTemperature = "Température"; +byte WifiSleep = 1; +byte pSerial = 2; //Choix Pin port serie +byte pTriac = 2; //Choix Pin Triac +String Source_Temp = "tempNo"; +String GS = String((char)29); //Group Separator +String RS = String((char)30); //Record Separator +String MessageH[10]; +int idxMessage = 0; +int P_cent_EEPROM; +int cptLEDyellow = 0; +int cptLEDgreen = 0; + +unsigned int CalibU = 1000; //Calibration Routeur UxI +unsigned int CalibI = 1000; +int value0; +int volt[100]; +int amp[100]; +float KV = 0.2083; //Calibration coefficient for the voltage. Value for CalibU=1000 at startup +float KI = 0.0642; //Calibration coefficient for the current. Value for CalibI=1000 at startup +float kV = 0.2083; //Calibration coefficient for the voltage. Corrected value +float kI = 0.0642; //Calibration coefficient for the current. Corrected value +float voltM[100]; //Voltage Mean value +float ampM[100]; + +bool EnergieActiveValide = false; +long EAS_T_J0 = 0; +long EAI_T_J0 = 0; +long EAS_M_J0 = 0; //Debut du jour energie active +long EAI_M_J0 = 0; + + +int adr_debut_para = 0; //Adresses Para après le Wifi + + +//Paramètres électriques +float Tension_T, Intensite_T, PowerFactor_T, Frequence; +float Tension_M, Intensite_M, PowerFactor_M; +long Energie_T_Soutiree = 0; +long Energie_T_Injectee = 0; +long Energie_M_Soutiree = 0; +long Energie_M_Injectee = 0; +long EnergieJour_T_Injectee = 0; +long EnergieJour_M_Injectee = 0; +long EnergieJour_T_Soutiree = 0; +long EnergieJour_M_Soutiree = 0; +int PuissanceS_T, PuissanceS_M, PuissanceI_T, PuissanceI_M; +int PVAS_T, PVAS_M, PVAI_T, PVAI_M; +float PuissanceS_T_inst, PuissanceS_M_inst, PuissanceI_T_inst, PuissanceI_M_inst; +float PVAS_T_inst, PVAS_M_inst, PVAI_T_inst, PVAI_M_inst; +float Puissance_T_moy, Puissance_M_moy; +float PVA_T_moy, PVA_M_moy; +float EASfloat = 0; +float EAIfloat = 0; +int PactConso_M, PactProd; +int tabPw_Maison_5mn[600]; //Puissance Active:Soutiré-Injecté toutes les 5mn +int tabPw_Triac_5mn[600]; +int tabTemperature_5mn[600]; +int tabPw_Maison_2s[300]; //Puissance Active: toutes les 2s +int tabPw_Triac_2s[300]; //Puissance Triac: toutes les 2s +int tabPva_Maison_2s[300]; //Puissance Active: toutes les 2s +int tabPva_Triac_2s[300]; +int tabPulseSinusOn[101]; +int tabPulseSinusTotal[101]; +int tab_histo_ouverture[LesActionsLength][600]; +int IdxStock2s = 0; +int IdxStockPW = 0; +float PmaxReseau = 36000; //Puissance Max pour eviter des débordements +bool LissageLong = false; +bool Pva_valide = false; +int RXD2, TXD2; //Port serie +int pulseTriac, zeroCross; + +//Parameters for JSY-MK-194T module +byte ByteArray[130]; +long LesDatas[14]; +int Sens_1, Sens_2; + + +//Parameters for JSY-MK-333 module triphasé +String MK333_dataBrute = ""; +// ajout PhDV61 compteur d'énergie quotidienne soutirée et injectée comme calculées par le Linky +float Energie_jour_Soutiree = 0; +float Energie_jour_Injectee = 0; +long Temps_precedent = 0; // mesure précise du temps entre deux appels au JSY-MK-333 + +//Parameters for Linky +bool LFon = false; +bool EASTvalid = false; +bool EAITvalid = false; +volatile int IdxDataRawLinky = 0; +volatile int IdxBufDecodLinky = 0; +volatile char DataRawLinky[10000]; //Buffer entrée données Linky +float moyPWS = 0; +float moyPWI = 0; +float moyPVAS = 0; +float moyPVAI = 0; +float COSphiS = 1; +float COSphiI = 1; +long TlastEASTvalide = 0; +long TlastEAITvalide = 0; +String LTARF = ""; //Option tarifaire EDF +String STGE = ""; //Status Tempo uniquement EDF + +//Paramètres for Enphase-Envoy-Smetered +String TokenEnphase = ""; +String EnphaseUser = ""; +String EnphasePwd = ""; +String EnphaseSerial = "0"; //Sert égalemnet au Shelly comme numéro de voie +String JsonToken = ""; +String Session_id = ""; +long LastwhDlvdCum = 0; //Dernière valeur cumul Wh Soutire-injecté. +float EMI_Wh = 0; //Energie entrée Maison Injecté Wh +float EMS_Wh = 0; //Energie entrée Maison Soutirée Wh + +//Paramètres for SmartGateways +String SG_dataBrute = ""; + +//Paramètres for Shelly Em +String ShEm_dataBrute = ""; +int ShEm_comptage_appels = 0; +float PwMoy2 = 0; //Moyenne voie secondsaire +float pfMoy2 = 1; //pf Voie secondaire + +//Paramètres pour puissance via MQTT +String P_MQTT_Brute = ""; +float PwMQTT = 0; +float PvaMQTT = 0; +float PfMQTT = 1; + +//Paramètres pour EDF +String DateEDF = ""; //an-mois-jour +byte TempoEDFon = 0; +int LastHeureEDF = -1; +int LTARFbin = 0; //Code binaire des tarifs + + + +//Actions +Action LesActions[LesActionsLength]; //Liste des actions +volatile int NbActions = 0; + + + +//Internal Timers +unsigned long startMillis; +unsigned long previousWifiMillis; +unsigned long previousHistoryMillis; +unsigned long previousWsMillis; +unsigned long previousWiMillis; +unsigned long LastRMS_Millis; +unsigned long previousTimer2sMillis; +unsigned long previousOverProdMillis; +unsigned long previousLEDsMillis; +unsigned long previousActionMillis; +unsigned long previousTempMillis; +unsigned long previousLoop; +unsigned long previousETX; +unsigned long PeriodeProgMillis = 1000; +unsigned long T0_seconde = 0; +unsigned long T_On_seconde = 0; +float previousLoopMin = 1000; +float previousLoopMax = 0; +float previousLoopMoy = 0; +unsigned long previousTimeRMS; +float previousTimeRMSMin = 1000; +float previousTimeRMSMax = 0; +float previousTimeRMSMoy = 0; +unsigned long previousMQTTenvoiMillis; +unsigned long previousMQTTMillis; +unsigned long LastPwMQTTMillis = 0; + +//Actions et Triac(action 0) +float RetardF[LesActionsLength]; //Floating value of retard +//Variables in RAM for interruptions +volatile unsigned long lastIT = 0; +volatile int IT10ms = 0; //Interruption avant deglitch +volatile int IT10ms_in = 0; //Interruption apres deglitch +volatile int ITmode = 0; //IT exerne Triac ou interne +hw_timer_t *timer = NULL; +hw_timer_t *timer10ms = NULL; + + +volatile int Retard[LesActionsLength]; +volatile int Actif[LesActionsLength]; +volatile int PulseOn[LesActionsLength]; +volatile int PulseTotal[LesActionsLength]; +volatile int PulseComptage[LesActionsLength]; +volatile int Gpio[LesActionsLength]; +volatile int OutOn[LesActionsLength]; +volatile int OutOff[LesActionsLength]; + +WebServer server(80); // Simple Web Server on port 80 + +//Port Serie 2 - Remplace Serial2 qui bug +HardwareSerial MySerial(2); + +// Heure et Date +#define MAX_SIZE_T 80 +const char *ntpServer1 = "fr.pool.ntp.org"; +const char *ntpServer2 = "time.nist.gov"; +String DATE = ""; +String DateCeJour = ""; +bool DATEvalid = false; +int HeureCouranteDeci = 0; +int idxPromDuJour = 0; + +//Température Capteur DS18B20 +OneWire oneWire(pinTemp); +DallasTemperature ds18b20(&oneWire); +float temperature = -127; // La valeur vaut -127 quand la sonde DS18B20 n'est pas présente +bool ds18b20_Init = false; +int TemperatureValide = 0; + + +//MQTT +WiFiClient MqttClient; +PubSubClient clientMQTT(MqttClient); + +//WIFI +int WIFIbug = 0; +int ComBug = 0; +WiFiClientSecure clientSecu; +WiFiClientSecure clientSecuEDF; +String Liste_AP = ""; + + +//Multicoeur - Processeur 0 - Collecte données RMS local ou distant +TaskHandle_t Task1; +esp_err_t ESP32_ERROR; +bool FirstLoop0 = false; + +//Interruptions, Current Zero Crossing from Triac device and Internal Timer +//************************************************************************* +void IRAM_ATTR onTimer10ms() { //Interruption interne toutes 10ms + ITmode = ITmode - 1; + if (ITmode < -5) ITmode = -5; + if (ITmode < 0) GestionIT_10ms(); //IT non synchrone avec le secteur . Horloge interne +} + + +// Interruption du Triac Signal Zc, toutes les 10ms +void IRAM_ATTR currentNull() { + IT10ms = IT10ms + 1; + if ((millis() - lastIT) > 2) { // to avoid glitch detection during 2ms + ITmode = ITmode + 2; + if (ITmode > 5) ITmode = 5; + IT10ms_in = IT10ms_in + 1; + lastIT = millis(); + if (ITmode > 0) GestionIT_10ms(); //IT synchrone avec le secteur signal Zc + } +} + + +void GestionIT_10ms() { + for (int i = 0; i < NbActions; i++) { + switch (Actif[i]) { //valeur en RAM + case 0: //Inactif + + break; + case 1: //Decoupe Sinus uniquement pour Triac + if (i == 0) { + PulseComptage[0] = 0; + digitalWrite(pulseTriac, LOW); //Stop Découpe Triac + } + break; + default: // Multi Sinus ou Train de sinus + if (Gpio[i] > 0) { //Gpio valide + if (PulseComptage[i] < PulseOn[i]) { + digitalWrite(Gpio[i], OutOn[i]); + } else { + digitalWrite(Gpio[i], OutOff[i]); //Stop + } + PulseComptage[i] = PulseComptage[i] + 1; + if (PulseComptage[i] >= PulseTotal[i]) { + PulseComptage[i] = 0; + } + } + break; + } + } +} + +// Interruption Timer interne toutes les 100 micro secondes +void IRAM_ATTR onTimer() { //Interruption every 100 micro second + if (Actif[0] == 1) { // Découpe Sinus + PulseComptage[0] = PulseComptage[0] + 1; + if (PulseComptage[0] > Retard[0] && Retard[0] < 98 && ITmode > 0) { //100 steps in 10 ms + digitalWrite(pulseTriac, HIGH); //Activate Triac + } else { + digitalWrite(pulseTriac, LOW); //Stop Triac + } + } +} + +// SETUP +//******* +void setup() { + startMillis = millis(); + previousLEDsMillis = startMillis; + + //Pin initialisation + pinMode(LedYellow, OUTPUT); + pinMode(LedGreen, OUTPUT); + digitalWrite(LedYellow, LOW); + digitalWrite(LedGreen, LOW); + + //Ports Série ESP + Serial.begin(115200); + Serial.println("Booting"); + + + //Watchdog initialisation + // Initialisation de la structure de configuration pour la WDT + esp_task_wdt_config_t wdt_config = { + .timeout_ms = WDT_TIMEOUT * 1000, // Convertir le temps en millisecondes + .idle_core_mask = (1 << portNUM_PROCESSORS) - 1, // Bitmask of all cores, https://github.com/espressif/esp-idf/blob/v5.2.2/examples/system/task_watchdog/main/task_watchdog_example_main.c + .trigger_panic = true // Enable panic to restart ESP32 + }; + // Initialisation de la WDT avec la structure de configuration + ESP32_ERROR = esp_task_wdt_init(&wdt_config); + StockMessage("Dernier Reset : " + String(esp_err_to_name(ESP32_ERROR))); + + + + for (int i = 0; i < LesActionsLength; i++) { + LesActions[i] = Action(i); //Creation objets + PulseOn[i] = 0; //1/2 sinus + PulseTotal[i] = 100; + PulseComptage[i] = 0; + Retard[i] = 100; + RetardF[i] = 100; + OutOn[i] = 1; + OutOff[i] = 0; + Gpio[i] = -1; + } + + + //Tableau Longueur Pulse et Longueur Trame pour Multi-Sinus de 0 à 100% + float erreur; + float vrai; + float target; + for (int I = 0; I < 101; I++) { + tabPulseSinusTotal[I] = -1; + tabPulseSinusOn[I] = -1; + target = float(I) / 100.0; + for (int T = 20; T < 101; T++) { + for (int N = 0; N <= T; N++) { + if (T % 2 == 1 || N % 2 == 0) { // Valeurs impair du total ou pulses pairs pour éviter courant continu + vrai = float(N) / float(T); + erreur = abs(vrai - target); + if (erreur < 0.004) { + tabPulseSinusTotal[I] = T; + tabPulseSinusOn[I] = N; + N = 101; + T = 101; + } + } + } + } + } + + init_puissance(); + //Liste Wifi à faire avant connexion à un AP. Necessaire depuis biblio ESP32 3.0.1 + WiFi.mode(WIFI_STA); + WiFi.disconnect(); + Liste_WIFI(); + + + Serial.print("Version : "); + Serial.println(Version); + // Configure WIFI + // ************** + String hostname(HOSTNAME); + uint32_t chipId = 0; + for (int i = 0; i < 17; i = i + 8) { + chipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i; + } + hostname += String(chipId); //Add chip ID to hostname + WiFi.hostname(hostname); + Serial.println(hostname); + ap_default_ssid = (const char *)hostname.c_str(); + // Check WiFi connection + // ... check mode + if (WiFi.getMode() != WIFI_STA) { + WiFi.mode(WIFI_STA); + delay(10); + } + + INIT_EEPROM(); + //Lecture Clé pour identifier si la ROM a déjà été initialisée + Cle_ROM = CLE_Rom_Init; + unsigned long Rcle = LectureCle(); + Serial.println("cle : " + String(Rcle)); + if (Rcle == Cle_ROM) { // Programme déjà executé + LectureEnROM(); + LectureConsoMatinJour(); + InitGpioActions(); + } else { + RAZ_Histo_Conso(); + } + + //Triac init + if (pTriac > 0) { + pulseTriac = pulseTriac_2; + zeroCross = zeroCross_2; + if (pTriac == 1) { + pulseTriac = pulseTriac_1; + zeroCross = zeroCross_1; + } + pinMode(zeroCross, INPUT_PULLDOWN); + pinMode(pulseTriac, OUTPUT); + digitalWrite(pulseTriac, LOW); //Stop Triac + } else { + Actif[0] = 0; + LesActions[0].Actif = 0; + } + Gpio[0] = pulseTriac; + LesActions[0].Gpio = pulseTriac; + + //Heure / Hour . A Mettre en priorité avant WIFI (exemple ESP32 Simple Time) + //External timer to obtain the Hour and reset Watt Hour every day at 0h + sntp_set_time_sync_notification_cb(time_sync_notification); + //sntp_servermode_dhcp(1); Déprecié + esp_sntp_servermode_dhcp(true); //Option + configTzTime("CET-1CEST-2,M3.5.0/02:00:00,M10.5.0/03:00:00", ntpServer1, ntpServer2); //Voir Time-Zone: https://sites.google.com/a/usapiens.com/opnode/time-zones + + + + //WIFI + Serial.println("SSID:" + ssid); + Serial.println("Pass:" + password); + if (ssid.length() > 0) { + if (dhcpOn == 0) { //Static IP + byte arr[4]; + arr[0] = IP_Fixe & 0xFF; // 0x78 + arr[1] = (IP_Fixe >> 8) & 0xFF; // 0x56 + arr[2] = (IP_Fixe >> 16) & 0xFF; // 0x34 + arr[3] = (IP_Fixe >> 24) & 0xFF; // 0x12 + // Set your Static IP address + IPAddress local_IP(arr[3], arr[2], arr[1], arr[0]); + // Set your Gateway IP address + arr[0] = Gateway & 0xFF; // 0x78 + arr[1] = (Gateway >> 8) & 0xFF; // 0x56 + arr[2] = (Gateway >> 16) & 0xFF; // 0x34 + arr[3] = (Gateway >> 24) & 0xFF; // 0x12 + IPAddress gateway(arr[3], arr[2], arr[1], arr[0]); + // Set your masque/subnet IP address + arr[0] = masque & 0xFF; + arr[1] = (masque >> 8) & 0xFF; + arr[2] = (masque >> 16) & 0xFF; + arr[3] = (masque >> 24) & 0xFF; + IPAddress subnet(arr[3], arr[2], arr[1], arr[0]); + // Set your DNS IP address + arr[0] = dns & 0xFF; + arr[1] = (dns >> 8) & 0xFF; + arr[2] = (dns >> 16) & 0xFF; + arr[3] = (dns >> 24) & 0xFF; + IPAddress primaryDNS(arr[3], arr[2], arr[1], arr[0]); //optional + IPAddress secondaryDNS(8, 8, 4, 4); //optional + if (!WiFi.config(local_IP, gateway, subnet, primaryDNS, secondaryDNS)) { + Serial.println("WIFI STA Failed to configure"); + } + } + StockMessage("Wifi Begin : " + ssid); + WiFi.begin(ssid.c_str(), password.c_str()); + WiFi.setSleep(WifiSleep); + while (WiFi.status() != WL_CONNECTED && (millis() - startMillis < 20000)) { // Attente connexion au Wifi + Serial.write('.'); + Gestion_LEDs(); + Serial.print(WiFi.status()); + delay(300); + } + Serial.println(); + } + if (WiFi.status() == WL_CONNECTED) { + StockMessage("Connected IP address: " + WiFi.localIP().toString() + " or " + hostname + ".local"); + } else { + StockMessage("Can not connect to WiFi station. Go into AP mode and STA mode."); + // Go into software AP and STA modes. + //WiFi.disconnect(); + delay(100); + WiFi.mode(WIFI_AP_STA); + delay(10); + WiFi.softAP(ap_default_ssid, ap_default_psk); + Serial.print("Access Point Mode. IP address: "); + Serial.println(WiFi.softAPIP()); + } + + + Init_Server(); + + + // Modification du programme par le Wifi - OTA(On The Air) + //*************************************************** + ArduinoOTA.setHostname((const char *)hostname.c_str()); + ArduinoOTA.begin(); //Mandatory + + + + //Adaptation à la Source + Serial.println("Source : " + Source); + + if (Source == "UxI") { + Setup_UxI(); + } + + if (Source == "Enphase") { + Setup_Enphase(); + } + + + if (Source == "Pmqtt") { + GestionMQTT(); + } + + //Port Série si besoin + if (pSerial > 0) { + RXD2 = RXD2_2; + TXD2 = TXD2_2; + if (pSerial == 1) { + RXD2 = RXD2_1; + TXD2 = TXD2_1; + } + if (Source == "UxIx2") { + Setup_UxIx2(); + } + if (Source == "UxIx3") { + Setup_JSY333(); + } + if (Source == "Linky") { + Setup_Linky(); + } + } + + if (Source == "Ext") { + } else { + Source_data = Source; + } + + + + + xTaskCreatePinnedToCore( //Préparation Tâche Multi Coeur + Task_LectureRMS, /* Task function. */ + "Task_LectureRMS", /* name of task. */ + 10000, /* Stack size of task */ + NULL, /* parameter of the task */ + 10, /* priority of the task */ + &Task1, /* Task handle to keep track of created task */ + 0); /* pin task to core 0 */ + + + if (pTriac > 0) { + //Interruptions du Triac et Timer interne + attachInterrupt(zeroCross, currentNull, RISING); + } + + //Hardware timer 100uS + timer = timerBegin(1000000); //Clock 1MHz + timerAttachInterrupt(timer, &onTimer); + timerAlarm(timer, 100, true, 0); //Interrupt every 100 microsecond + + //Hardware timer 10ms + timer10ms = timerBegin(1000000); //Clock 1MHz + timerAttachInterrupt(timer10ms, &onTimer10ms); + timerAlarm(timer10ms, 10000, true, 0); //Interrupt every 10ms + + + //Timers + previousWifiMillis = millis() - 25000; + previousHistoryMillis = millis() - 290000; + previousTimer2sMillis = millis(); + previousLoop = millis(); + previousTimeRMS = millis(); + previousMQTTenvoiMillis = millis(); + previousMQTTMillis = millis(); + previousETX = millis(); + previousOverProdMillis = millis(); + LastRMS_Millis = millis(); + previousActionMillis = millis(); + previousTempMillis = millis() - 118000; +} + +/* ********************** + * ****************** * + * * Tâches Coeur 0 * * + * ****************** * + ********************** +*/ + +void Task_LectureRMS(void *pvParameters) { + esp_task_wdt_add(NULL); //add current thread to WDT watch + esp_task_wdt_reset(); + for (;;) { + if (!FirstLoop0) { + Serial.println("FirstLoop0"); + FirstLoop0 = true; + ComOK(); + } + unsigned long tps = millis(); + float deltaT = float(tps - previousTimeRMS); + previousTimeRMS = tps; + previousTimeRMSMin = min(previousTimeRMSMin, deltaT); + previousTimeRMSMin = previousTimeRMSMin + 0.002; + previousTimeRMSMax = max(previousTimeRMSMax, deltaT); + previousTimeRMSMax = previousTimeRMSMax * 0.999; + previousTimeRMSMoy = deltaT * 0.01 + previousTimeRMSMoy * 0.99; + previousTimeRMSMin = min(previousTimeRMSMin, previousTimeRMSMoy); + previousTimeRMSMax = max(previousTimeRMSMax, previousTimeRMSMoy); + + + //Recupération des données RMS + //****************************** + if (tps - LastRMS_Millis > PeriodeProgMillis) { //Attention delicat pour eviter pb overflow + LastRMS_Millis = tps; + unsigned long ralenti = long(PuissanceS_M / 10); // On peut ralentir échange sur Wifi si grosse puissance en cours + if (Source == "UxI") { + LectureUxI(); + PeriodeProgMillis = 40; + } + if (pSerial > 0) { + if (Source == "UxIx2") { + LectureUxIx2(); + PeriodeProgMillis = 400; + } + if (Source == "UxIx3") { + Lecture_JSY333(); + PeriodeProgMillis = 600; + } + if (Source == "Linky") { + LectureLinky(); + PeriodeProgMillis = 2; + } + } + if (Source == "Enphase") { + LectureEnphase(); + LastRMS_Millis = millis(); + PeriodeProgMillis = 600 + ralenti; //On s'adapte à la vitesse réponse Envoy-S metered + } + if (Source == "SmartG") { + LectureSmartG(); + LastRMS_Millis = millis(); + PeriodeProgMillis = 200 + ralenti; //On s'adapte à la vitesse réponse SmartGateways + } + if (Source == "ShellyEm") { + LectureShellyEm(); + LastRMS_Millis = millis(); + PeriodeProgMillis = 200 + ralenti; //On s'adapte à la vitesse réponse ShellyEm + } + + if (Source == "Ext") { + CallESP32_Externe(); + LastRMS_Millis = + PeriodeProgMillis = 200 + ralenti; //Après pour ne pas surchargé Wifi + } + if (Source == "Pmqtt") { + PeriodeProgMillis = 600; + LastRMS_Millis = millis(); + UpdatePmqtt(); + } + } + delay(2); + } +} + + + + +/* ********************** + * ****************** * + * * Tâches Coeur 1 * * + * ****************** * + ********************** +*/ +void loop() { + //Estimation charge coeur + unsigned long tps = millis(); + float deltaT = float(tps - previousLoop); + previousLoop = tps; + previousLoopMin = min(previousLoopMin, deltaT); + previousLoopMin = previousLoopMin + 0.002; + previousLoopMax = max(previousLoopMax, deltaT); + previousLoopMax = previousLoopMax * 0.999; + previousLoopMoy = deltaT * 0.01 + previousLoopMoy * 0.99; + previousLoopMin = min(previousLoopMin, previousLoopMoy); + previousLoopMax = max(previousLoopMax, previousLoopMoy); + //Gestion des serveurs + //******************** + ArduinoOTA.handle(); + server.handleClient(); + + //Archivage et envois des mesures périodiquement + //********************************************** + if (EnergieActiveValide) { + if (tps - previousHistoryMillis >= 300000) { //Historique consommation par pas de 5mn + previousHistoryMillis = tps; + tabPw_Maison_5mn[IdxStockPW] = PuissanceS_M - PuissanceI_M; + tabPw_Triac_5mn[IdxStockPW] = PuissanceS_T - PuissanceI_T; + if (temperature > -20) { + tabTemperature_5mn[IdxStockPW] = int(temperature * 10); + } else { + tabTemperature_5mn[IdxStockPW] = 0; + } + + + for (int i = 0; i < NbActions; i++) { + if (Actif[i] > 0) { + tab_histo_ouverture[i][IdxStockPW] = 100 - Retard[i]; + } else { + tab_histo_ouverture[i][IdxStockPW] = 0; + } + } + IdxStockPW = (IdxStockPW + 1) % 600; + } + + + if (tps - previousTimer2sMillis >= 2000) { + previousTimer2sMillis = tps; + tabPw_Maison_2s[IdxStock2s] = PuissanceS_M - PuissanceI_M; + tabPw_Triac_2s[IdxStock2s] = PuissanceS_T - PuissanceI_T; + tabPva_Maison_2s[IdxStock2s] = PVAS_M - PVAI_M; + tabPva_Triac_2s[IdxStock2s] = PVAS_T - PVAI_T; + IdxStock2s = (IdxStock2s + 1) % 300; + JourHeureChange(); + EnergieQuotidienne(); + } + + if (tps - previousOverProdMillis >= 200) { + previousOverProdMillis = tps; + GestionOverproduction(); + } + } + if (tps - previousMQTTMillis > 200) { + previousMQTTMillis = tps; + GestionMQTT(); + } + if (tps - previousLEDsMillis >= 50) { + previousLEDsMillis = tps; + Gestion_LEDs(); + } + //Actions forcées et température + if (tps - previousActionMillis > 60000) { + previousActionMillis = tps; + + for (int i = 0; i < NbActions; i++) { + if (LesActions[i].tOnOff > 0) LesActions[i].tOnOff -= 1; + if (LesActions[i].tOnOff < 0) LesActions[i].tOnOff += 1; + } + } + if (tps - previousTempMillis > 120001) { + previousTempMillis = tps; + //Temperature + LectureTemperature(); + } + //Vérification du WIFI + //******************** + if (tps - previousWifiMillis > 30000) { //Test présence WIFI toutes les 30s et autres + previousWifiMillis = tps; + if (WiFi.getMode() == WIFI_STA) { + if (WiFi.waitForConnectResult(10000) != WL_CONNECTED) { + StockMessage("WIFI Connection Failed! #" + String(WIFIbug) + "ComBug #" + String(ComBug)); + WIFIbug++; + if (WIFIbug > 4) { + ESP.restart(); + } + } else { + WIFIbug = 0; + } + + + Serial.print("Niveau Signal WIFI:"); + Serial.println(WiFi.RSSI()); + Serial.print("IP address_: "); + Serial.println(WiFi.localIP()); + Serial.print("WIFIbug : #"); + Serial.println(WIFIbug); + Serial.print("ComBug : #"); + Serial.println(ComBug); + Serial.println("Charge Lecture RMS (coeur 0) en ms - Min : " + String(int(previousTimeRMSMin)) + " Moy : " + String(int(previousTimeRMSMoy)) + " Max : " + String(int(previousTimeRMSMax))); + Serial.println("Charge Boucle générale (coeur 1) en ms - Min : " + String(int(previousLoopMin)) + " Moy : " + String(int(previousLoopMoy)) + " Max : " + String(int(previousLoopMax))); + + int T = int(millis() / 1000); + float DureeOn = float(T) / 3600; + Serial.println("ESP32 ON depuis : " + String(DureeOn) + " heures"); + + JourHeureChange(); + Call_EDF_data(); + int Ltarf = 0; //Code binaire Tarif + if (LTARF.indexOf("PLEINE") >= 0) Ltarf += 1; + if (LTARF.indexOf("CREUSE") >= 0) Ltarf += 2; + if (LTARF.indexOf("BLEU") >= 0) Ltarf += 4; + if (LTARF.indexOf("BLANC") >= 0) Ltarf += 8; + if (LTARF.indexOf("ROUGE") >= 0) Ltarf += 16; + LTARFbin = Ltarf; + //Test pulse Zc Triac + if (ITmode < 0 && pTriac > 0) StockMessage("Erreur : pas de signal Zc du gradateur/Triac"); + } else { + Serial.print("Access Point Mode. IP address: "); + Serial.println(WiFi.softAPIP()); + } + } + if ((tps - startMillis) > 240000 && WiFi.getMode() != WIFI_STA) { //Connecté en Access Point depuis 4mn. Pas normal + Serial.println("Pas connecté en WiFi mode Station. Redémarrage"); + delay(5000); + ESP.restart(); + } +} + +// ************ +// * ACTIONS * +// ************ +void GestionOverproduction() { + float SeuilPw; + float MaxTriacPw; + float GainBoucle; + int Type_En_Cours = 0; + bool lissage = false; + //Puissance est la puissance en entrée de maison. >0 si soutire. <0 si injecte + //Cas du Triac. Action 0 + float Puissance = float(PuissanceS_M - PuissanceI_M); + if (NbActions == 0) LissageLong = true; //Cas d'un capteur seul et actions déporté sur autre ESP + for (int i = 0; i < NbActions; i++) { + Actif[i] = LesActions[i].Actif; //0=Inactif,1=Decoupe ou On/Off, 2=Multi, 3= Train + if (Actif[i] >= 2) lissage = true; //En RAM + Type_En_Cours = LesActions[i].TypeEnCours(HeureCouranteDeci, temperature, LTARFbin); //0=NO,1=OFF,2=ON,3=PW,4=Triac + if (Actif[i] > 0 && Type_En_Cours > 1 && DATEvalid) { // On ne traite plus le NO + if (Type_En_Cours == 2) { + RetardF[i] = 0; + } else { // 3 ou 4 + SeuilPw = float(LesActions[i].Valmin(HeureCouranteDeci)); + MaxTriacPw = float(LesActions[i].Valmax(HeureCouranteDeci)); + GainBoucle = float(LesActions[i].Reactivite); //Valeur stockée dans Port + if (Actif[i] == 1 && i > 0) { //Les relais en On/Off + if (Puissance > MaxTriacPw) { RetardF[i] = 100; } //OFF + if (Puissance < SeuilPw) { RetardF[i] = 0; } //On + } else { // le Triac ou les relais en sinus + RetardF[i] = RetardF[i] + 0.0001; //On ferme très légèrement si pas de message reçu. Sécurité + RetardF[i] = RetardF[i] + (Puissance - SeuilPw) * GainBoucle / 10000; // Gain de boucle de l'asservissement + if (RetardF[i] < 100 - MaxTriacPw) { RetardF[i] = 100 - MaxTriacPw; } + if (ITmode < 0 && i == 0) RetardF[i] = 100; //Triac pas possible sur synchro interne + } + if (RetardF[i] < 0) { RetardF[i] = 0; } + if (RetardF[i] > 100) { RetardF[i] = 100; } + } + } else { + RetardF[i] = 100; + } + Retard[i] = int(RetardF[i]); //Valeure entiere pour piloter le Triac et les relais + if (Retard[i] == 100) { // Force en cas d'arret des IT + LesActions[i].Arreter(); + PulseOn[i] = 0; //Stop Triac ou relais + } else { + + switch (Actif[i]) { //valeur en RAM du Mode de regulation + case 1: //Decoupe Sinus pour Triac ou On/Off pour relais + if (i > 0) LesActions[i].RelaisOn(); + break; + case 2: // Multi Sinus + PulseOn[i] = tabPulseSinusOn[100 - Retard[i]]; + PulseTotal[i] = tabPulseSinusTotal[100 - Retard[i]]; + break; + case 3: // Train de Sinus + PulseOn[i] = 100 - Retard[i]; + PulseTotal[i] = 99; //Nombre impair pour éviter courant continu + break; + } + } + } + LissageLong = lissage; +} + +void InitGpioActions() { + for (int i = 1; i < NbActions; i++) { + LesActions[i].InitGpio(); + Gpio[i] = LesActions[i].Gpio; + OutOn[i] = LesActions[i].OutOn; + OutOff[i] = LesActions[i].OutOff; + } +} +// *********************************** +// * Calage Zéro Energie quotidienne * - +// *********************************** + +void EnergieQuotidienne() { + if (DATEvalid && Source != "Ext") { + if (Energie_M_Soutiree < EAS_M_J0 || EAS_M_J0 == 0) { + EAS_M_J0 = Energie_M_Soutiree; + } + EnergieJour_M_Soutiree = Energie_M_Soutiree - EAS_M_J0; + if (Energie_M_Injectee < EAI_M_J0 || EAI_M_J0 == 0) { + EAI_M_J0 = Energie_M_Injectee; + } + EnergieJour_M_Injectee = Energie_M_Injectee - EAI_M_J0; + if (Energie_T_Soutiree < EAS_T_J0 || EAS_T_J0 == 0) { + EAS_T_J0 = Energie_T_Soutiree; + } + EnergieJour_T_Soutiree = Energie_T_Soutiree - EAS_T_J0; + if (Energie_T_Injectee < EAI_T_J0 || EAI_T_J0 == 0) { + EAI_T_J0 = Energie_T_Injectee; + } + EnergieJour_T_Injectee = Energie_T_Injectee - EAI_T_J0; + } +} + +// ************** +// * Heure DATE * - +// ************** +void time_sync_notification(struct timeval *tv) { + Serial.println("Notification de l'heure ( time synchronization event ) "); + DATEvalid = true; + Serial.print("Sync time in ms : "); + Serial.println(sntp_get_sync_interval()); + JourHeureChange(); + StockMessage("Réception de l'heure"); +} + + +//**************** +//* Gestion LEDs * +//**************** +void Gestion_LEDs() { + int retard_min = 100; + int retardI; + cptLEDyellow++; + if (WiFi.status() != WL_CONNECTED) { // Attente connexion au Wifi + if (WiFi.getMode() == WIFI_STA) { // en Station mode + cptLEDyellow = (cptLEDyellow + 6) % 10; + cptLEDgreen = cptLEDyellow; + } else { //AP Mode + cptLEDyellow = cptLEDyellow % 10; + cptLEDgreen = (cptLEDyellow + 5) % 10; + } + } else { + for (int i = 0; i < NbActions; i++) { + retardI = Retard[i]; + retard_min = min(retard_min, retardI); + } + if (retard_min < 100) { + cptLEDgreen = int((cptLEDgreen + 1 + 8 / (1 + retard_min / 10))) % 10; + } else { + cptLEDgreen = 10; + } + } + if (cptLEDyellow > 5) { + digitalWrite(LedYellow, LOW); + } else { + digitalWrite(LedYellow, HIGH); + } + if (cptLEDgreen > 5) { + digitalWrite(LedGreen, LOW); + } else { + digitalWrite(LedGreen, HIGH); + } +} +//************* +//* Test Pmax * +//************* +float PfloatMax(float Pin) { + float P = max(-PmaxReseau, Pin); + P = min(PmaxReseau, P); + return P; +} +int PintMax(int Pin) { + int M = int(PmaxReseau); + int P = max(-M, Pin); + P = min(M, P); + return P; +} +//**************************************************** +//* Comportement etrange depuis V3.0.1 de l'ESP32 * +// le watchdog se reset si le wifi plante. A creuser * +// Work around en attendant * +//**************************************************** +void ComAbuge() { + ComBug++; + if (ComBug < 200) { + esp_task_wdt_reset(); + } +} +void ComOK() { + ComBug = 0; + esp_task_wdt_reset(); // Reset du Watchdog +} \ No newline at end of file diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/Source_EnphaseEnvoy.ino b/docs/routers/F1ATB/Solar_Router_V10.00/Source_EnphaseEnvoy.ino new file mode 100644 index 0000000..6f1812d --- /dev/null +++ b/docs/routers/F1ATB/Solar_Router_V10.00/Source_EnphaseEnvoy.ino @@ -0,0 +1,300 @@ + +void Setup_Enphase() { + + //Obtention Session ID + //******************** + const char* server1Enphase = "enlighten.enphaseenergy.com"; + String Host = String(server1Enphase); + String adrEnphase = "https://" + Host + "/login/login.json"; + String requestBody = "user[email]=" + EnphaseUser + "&user[password]=" + urlEncode( EnphasePwd); + + if (EnphaseUser != "" && EnphasePwd != "") { + Serial.println("Essai connexion Enlighten server 1 pour obtention session_id!"); + clientSecu.setInsecure(); //skip verification + if (!clientSecu.connect(server1Enphase, 443)) + StockMessage("Connection failed to Enlighten server :" + Host); + else { + Serial.println("Connected to Enlighten server:" + Host); + clientSecu.println("POST " + adrEnphase + "?" + requestBody + " HTTP/1.0"); + clientSecu.println("Host: " + Host); + clientSecu.println("Connection: close"); + clientSecu.println(); + String line = ""; + while (clientSecu.connected()) { + line = clientSecu.readStringUntil('\n'); + if (line == "\r") { + Serial.println("headers 1 Enlighten received"); + JsonToken = ""; + } + + JsonToken += line; + } + // if there are incoming bytes available + // from the server, read them and print them: + while (clientSecu.available()) { + char c = clientSecu.read(); + Serial.write(c); + } + clientSecu.stop(); + } + Session_id = StringJson("session_id", JsonToken); + Serial.println("session_id :" + Session_id); + } else { + Serial.println("Connexion vers Envoy-S en firmware version 5"); + } + //Obtention Token + //******************** + if (Session_id != "" && EnphaseSerial != "" && EnphaseUser != "") { + const char* server2Enphase = "entrez.enphaseenergy.com"; + Host = String(server2Enphase); + adrEnphase = "https://" + Host + "/tokens"; + requestBody = "{\"session_id\":\"" + Session_id + "\", \"serial_num\":" + EnphaseSerial + ", \"username\":\"" + EnphaseUser + "\"}"; + Serial.println("Essai connexion Enlighten server 2 pour obtention token!"); + clientSecu.setInsecure(); //skip verification + if (!clientSecu.connect(server2Enphase, 443)) + StockMessage("Connection failed to :" + Host); + else { + Serial.println("Connected to :" + Host); + clientSecu.println("POST " + adrEnphase + " HTTP/1.0"); + clientSecu.println("Host: " + Host); + clientSecu.println("Content-Type: application/json"); + clientSecu.println("content-length:" + String(requestBody.length())); + clientSecu.println("Connection: close"); + clientSecu.println(); + clientSecu.println(requestBody); + clientSecu.println(); + Serial.println("Attente user est connecté"); + String line = ""; + JsonToken = ""; + while (clientSecu.connected()) { + line = clientSecu.readStringUntil('\n'); + if (line == "\r") { + Serial.println("headers 2 enlighten received"); + JsonToken = ""; + } + + JsonToken += line; + } + // if there are incoming bytes available + // from the server, read them and print them: + while (clientSecu.available()) { + char c = clientSecu.read(); + Serial.write(c); + } + clientSecu.stop(); + JsonToken.trim(); + Serial.println("Token :" + JsonToken); + if (JsonToken.length() > 50) { + TokenEnphase = JsonToken; + previousTimeRMSMin = 1000; + previousTimeRMSMax = 1; + previousTimeRMSMoy = 1; + previousTimeRMS = millis(); + LastRMS_Millis = millis(); + PeriodeProgMillis = 1000; + } + } + } +} + +void LectureEnphase() { //Lecture des consommations + int Num_portIQ = 443; + String JsonEnPhase = ""; + byte arr[4]; + arr[0] = RMSextIP & 0xFF; // 0x78 + arr[1] = (RMSextIP >> 8) & 0xFF; // 0x56 + arr[2] = (RMSextIP >> 16) & 0xFF; // 0x34 + arr[3] = (RMSextIP >> 24) & 0xFF; // 0x12 + String host = String(arr[3]) + "." + String(arr[2]) + "." + String(arr[1]) + "." + String(arr[0]); + + if (TokenEnphase.length() > 50 && EnphaseUser != "") { //Connexion por firmware V7 + if (millis() > 2592000000) { //Tout les 30 jours on recherche un nouveau Token + Setup_Enphase(); + } + + clientSecu.setInsecure(); //skip verification + if (!clientSecu.connect(host.c_str(), Num_portIQ)) { + StockMessage("Connection failed to Envoy-S server! : " + host); + } else { + //Serial.println("Connected to Envoy-S server!"); + clientSecu.println("GET https://" + host + "/ivp/meters/reports/consumption HTTP/1.0"); + clientSecu.println("Host: " + host); + clientSecu.println("Accept: application/json"); + clientSecu.println("Authorization: Bearer " + TokenEnphase); + clientSecu.println("Connection: close"); + clientSecu.println(); + + String line = ""; + while (clientSecu.connected()) { + line = clientSecu.readStringUntil('\n'); + if (line == "\r") { + //Serial.println("headers received"); + JsonEnPhase = ""; + } + JsonEnPhase += line; + } + // if there are incoming bytes available + // from the server, read them and print them: + while (clientSecu.available()) { + char c = clientSecu.read(); + Serial.write(c); + } + + clientSecu.stop(); + } + } else { // Conexion Envoy V5 + // Use WiFiClient class to create TCP connections http + WiFiClient clientFirmV5; + if (!clientFirmV5.connect(host.c_str(), 80)) { + StockMessage("connection to client clientFirmV5 failed (call to Envoy-S)"); + delay(200); + ComAbuge(); + return; + } + String url = "/ivp/meters/reports/consumption"; + clientFirmV5.print(String("GET ") + url + " HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Connection: close\r\n\r\n");; + unsigned long timeout = millis(); + while (clientFirmV5.available() == 0) { + if (millis() - timeout > 5000) { + Serial.println(">>> client clientFirmV5 Timeout !"); + clientFirmV5.stop(); + return; + } + } + timeout = millis(); + String line; + // Lecture des données brutes distantes + while (clientFirmV5.available() && (millis() - timeout < 5000)) { + line = clientFirmV5.readStringUntil('\n'); + if (line == "\r") { + //Serial.println("headers received"); + JsonEnPhase = ""; + } + JsonEnPhase += line; + } + } + + // On utilise pas la librairie ArduinoJson.h, pour décoder message Json, qui crache sur de grosses données + String TotConso = PrefiltreJson("total-consumption", "cumulative", JsonEnPhase); + PactConso_M = int(ValJson("actPower", TotConso)); + String NetConso = PrefiltreJson("net-consumption", "cumulative", JsonEnPhase); + float PactReseau = ValJson("actPower", NetConso); + PactReseau = PfloatMax(PactReseau); + if (PactReseau < 0) { + PuissanceS_M_inst = 0; + PuissanceI_M_inst = int(-PactReseau); + } else { + PuissanceI_M_inst = 0; + PuissanceS_M_inst = int(PactReseau); + } + float PvaReseau = ValJson("apprntPwr", NetConso); + PvaReseau = PfloatMax(PvaReseau); + if (PvaReseau < 0) { + PVAS_M_inst = 0; + PVAI_M_inst = int(-PvaReseau); + } else { + PVAI_M_inst = 0; + PVAS_M_inst = int(PvaReseau); + } + Pva_valide=true; + filtre_puissance(); + float PowerFactor = 0; + if ((PVA_M_moy ) != 0) { + PowerFactor = floor(100 * abs(Puissance_M_moy ) / PVA_M_moy ) / 100; + PowerFactor = min(PowerFactor, float(1)); + } + PowerFactor_M = PowerFactor; + long whDlvdCum = LongJson("whDlvdCum", NetConso); + long DeltaWh = 0; + if (whDlvdCum != 0) { // bonne donnée + if (LastwhDlvdCum == 0) { + LastwhDlvdCum = whDlvdCum; + } + DeltaWh = whDlvdCum - LastwhDlvdCum; + LastwhDlvdCum = whDlvdCum; + if (DeltaWh < 0) { + Energie_M_Injectee = Energie_M_Injectee - DeltaWh; + } else { + Energie_M_Soutiree = Energie_M_Soutiree + DeltaWh; + } + } + Tension_M = ValJson("rmsVoltage", NetConso); + Intensite_M = ValJson("rmsCurrent", NetConso); + PactProd = PactConso_M - int(PactReseau); + EnergieActiveValide = true; + if (PactReseau != 0 || PvaReseau != 0) { + ComOK(); //Reset du Watchdog à chaque trame reçue de la passerelle Envoy-S metered + } + if (cptLEDyellow > 30) { + cptLEDyellow = 4; + } +} + +String PrefiltreJson(String F1, String F2, String Json) { + int p = Json.indexOf(F1); + Json = Json.substring(p); + p = Json.indexOf(F2); + Json = Json.substring(p); + return Json; +} + +float ValJson(String nom, String Json) { + int p = Json.indexOf(nom); + Json = Json.substring(p); + p = Json.indexOf(":"); + Json = Json.substring(p + 1); + int q = Json.indexOf(","); + p = Json.indexOf("}"); + p = min(p, q); + float val = 0; + if (p > 0) { + Json = Json.substring(0, p); + val = Json.toFloat(); + } + return val; +} +long LongJson(String nom, String Json) { // Pour éviter des problèmes d'overflow + int p = Json.indexOf(nom); + Json = Json.substring(p); + p = Json.indexOf(":"); + Json = Json.substring(p + 1); + int q = Json.indexOf("."); + p = Json.indexOf("}"); + p = min(p, q); + long val = 0; + if (p > 0) { + Json = Json.substring(0, p); + val = Json.toInt(); + } + return val; +} + +long myLongJson(String nom, String Json) { // Alternative a LongJson au dessus pour extraire chez EDF nb jour Tempo https://particulier.edf.fr/services/rest/referentiel/getNbTempoDays?TypeAlerte=TEMPO + int p = Json.indexOf(nom); + Json = Json.substring(p); + p = Json.indexOf(":"); + Json = Json.substring(p + 1); + int q = Json.indexOf(",");//<==== Recherche d'une virgule et non d'un point + if (q == -1) q = 999; // /<==== Ajout de ces 2 lignes pour que la ligne p = min(p, q); ci dessous donne le bon résultat + p = Json.indexOf("}"); + p = min(p, q); + long val = 0; + if (p > 0) { + Json = Json.substring(0, p); + val = Json.toInt(); + } + return val; +} + + +String StringJson(String nom, String Json) { + int p = Json.indexOf(nom); + Json = Json.substring(p); + p = Json.indexOf(":"); + Json = Json.substring(p + 1); + p = Json.indexOf("\""); + Json = Json.substring(p + 1); + p = Json.indexOf("\""); + Json = Json.substring(0, p); + return Json; +} diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/Source_Externe.ino b/docs/routers/F1ATB/Solar_Router_V10.00/Source_Externe.ino new file mode 100644 index 0000000..a3f65f3 --- /dev/null +++ b/docs/routers/F1ATB/Solar_Router_V10.00/Source_Externe.ino @@ -0,0 +1,143 @@ +// *************************************************************** +// * Client d'un autre ESP32 en charge de mesurer les puissances * +// *************************************************************** +void CallESP32_Externe() { + String S = ""; + String RMSExtDataB = ""; + String Gr[4]; + String data_[22]; + + + // Use WiFiClient class to create TCP connections + WiFiClient clientESP_RMS; + byte arr[4]; + arr[0] = RMSextIP & 0xFF; // 0x78 + arr[1] = (RMSextIP >> 8) & 0xFF; // 0x56 + arr[2] = (RMSextIP >> 16) & 0xFF; // 0x34 + arr[3] = (RMSextIP >> 24) & 0xFF; // 0x12 + + String host = String(arr[3]) + "." + String(arr[2]) + "." + String(arr[1]) + "." + String(arr[0]); + if (!clientESP_RMS.connect(host.c_str(), 80)) { + ComAbuge(); + StockMessage("connection to ESP_RMS : " + host +" failed"); + delay(200); + return; + } + String url = "/ajax_data"; + clientESP_RMS.print(String("GET ") + url + " HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Connection: close\r\n\r\n"); + unsigned long timeout = millis(); + while (clientESP_RMS.available() == 0) { + if (millis() - timeout > 5000) { + + StockMessage("client ESP_RMS Timeout !" + host); + + clientESP_RMS.stop(); + return; + } + } + timeout = millis(); + // Lecture des données brutes distantes + while (clientESP_RMS.available() && (millis() - timeout < 5000)) { + RMSExtDataB += clientESP_RMS.readStringUntil('\r'); + } + if (RMSExtDataB.length() > 300) { + RMSExtDataB = ""; + } + if (RMSExtDataB.indexOf("Deb") >= 0 && RMSExtDataB.indexOf("Fin") > 0) { //Trame complète reçue + RMSExtDataB = RMSExtDataB.substring(RMSExtDataB.indexOf("Deb") + 4); + RMSExtDataB = RMSExtDataB.substring(0, RMSExtDataB.indexOf("Fin") + 3); + String Sval = ""; + int idx = 0; + while (RMSExtDataB.indexOf(GS) > 0) { + Sval = RMSExtDataB.substring(0, RMSExtDataB.indexOf(GS)); + RMSExtDataB = RMSExtDataB.substring(RMSExtDataB.indexOf(GS) + 1); + Gr[idx] = Sval; + idx++; + } + Gr[idx] = RMSExtDataB; + idx = 0; + for (int i = 0; i < 3; i++) { + while (Gr[i].indexOf(RS) >= 0) { + Sval = Gr[i].substring(0, Gr[i].indexOf(RS)); + Gr[i] = Gr[i].substring(Gr[i].indexOf(RS) + 1); + data_[idx] = Sval; + idx++; + } + data_[idx] = Gr[i]; + idx++; + } + for (int i = 0; i <= idx; i++) { + switch (i) { + + case 1: + Source_data = data_[i]; + break; + case 2: + if (TempoEDFon == 0) LTARF = data_[i]; + break; + case 3: + if (TempoEDFon == 0) STGE = data_[i]; + break; + case 4: + //Temperature non utilisé + break; + case 5: + Pva_valide=data_[i].toInt(); + break; + case 6: + PuissanceS_M = PintMax(data_[i].toInt()); + break; + case 7: + PuissanceI_M = PintMax(data_[i].toInt()); + break; + case 8: + PVAS_M = PintMax(data_[i].toInt()); + break; + case 9: + PVAI_M = PintMax(data_[i].toInt()); + break; + case 10: + EnergieJour_M_Soutiree = data_[i].toInt(); + break; + case 11: + EnergieJour_M_Injectee = data_[i].toInt(); + break; + case 12: + Energie_M_Soutiree = data_[i].toInt(); + break; + case 13: + Energie_M_Injectee = data_[i].toInt(); + ComOK(); //Reset du Watchdog à chaque trame du RMS reçue + cptLEDyellow = 4; + EnergieActiveValide=true; + break; + case 14: //CAS UxIx2 avec une deuxieme sonde + PuissanceS_T = data_[i].toInt(); + break; + case 15: + PuissanceI_T = data_[i].toInt(); + break; + case 16: + PVAS_T = data_[i].toInt(); + break; + case 17: + PVAI_T = data_[i].toInt(); + break; + case 18: + EnergieJour_T_Soutiree = data_[i].toInt(); + break; + case 19: + EnergieJour_T_Injectee = data_[i].toInt(); + break; + case 20: + Energie_T_Soutiree = data_[i].toInt(); + break; + case 21: + Energie_T_Injectee = data_[i].toInt(); + break; + } + + } + RMSExtDataB = ""; + } +} diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/Source_Linky.ino b/docs/routers/F1ATB/Solar_Router_V10.00/Source_Linky.ino new file mode 100644 index 0000000..fa01904 --- /dev/null +++ b/docs/routers/F1ATB/Solar_Router_V10.00/Source_Linky.ino @@ -0,0 +1,181 @@ +// **************************** +// * Source de Mesures LINKY * +// **************************** + +float deltaWS = 0; +float deltaWI = 0; +int boucle_appel_Linky = 0; +void Setup_Linky() { + delay(20); + MySerial.setRxBufferSize(SER_BUF_SIZE); + MySerial.begin(9600, SERIAL_7E1, RXD2, TXD2); // 7-bit Even parity 1 stop bit pour le Linky + delay(100); +} + +void LectureLinky() { //Lecture port série du LINKY . + int V = 0; + long OldWh = 0; + float deltaWh = 0; + float Pmax = 0; + float Pmin = 0; + unsigned long Tm = 0; + float deltaT = 0; + boucle_appel_Linky++; + if (boucle_appel_Linky > 4000) { + boucle_appel_Linky = 0; + MySerial.flush(); + MySerial.write("Ok"); + StockMessage("Attente Linky 4000 boucles = 8s"); + } + while (MySerial.available() > 0) { + boucle_appel_Linky = 0; + V = MySerial.read(); + DataRawLinky[IdxDataRawLinky] = char(V); + IdxDataRawLinky = (IdxDataRawLinky + 1) % 10000; + switch (V) { + case 2: //STX (Start Text) + break; + case 3: //ETX (End Text) + previousETX = millis(); + cptLEDyellow = 4; + LFon = false; + break; + case 10: // Line Feed. Debut Groupe + LFon = true; + IdxBufDecodLinky = IdxDataRawLinky; + break; + case 13: // Line Feed. Debut Groupe + if (LFon) { //Debut groupe OK + LFon = false; + int nb_tab = 0; + String code = ""; + String val = ""; + int checksum = 0; + int checkLinky = -1; + + while (IdxBufDecodLinky != IdxDataRawLinky) { + if (DataRawLinky[IdxBufDecodLinky] == char(9)) { //Tabulation + nb_tab++; + } else { + if (nb_tab == 0) { + code += DataRawLinky[IdxBufDecodLinky]; + } + if (nb_tab == 1) { + val += DataRawLinky[IdxBufDecodLinky]; + } + if (nb_tab <= 1) { + checksum += (int)DataRawLinky[IdxBufDecodLinky]; + } + } + IdxBufDecodLinky = (IdxBufDecodLinky + 1) % 10000; + if (checkLinky == -1 && nb_tab == 2) { + checkLinky = (int)DataRawLinky[IdxBufDecodLinky]; + checksum += 18; //2 tabulations + checksum = checksum & 63; //0x3F + checksum = checksum + 32; //0x20 + } + } + if (code.indexOf("EAST") == 0 || code.indexOf("EAIT") == 0 || code == "SINSTS" || code.indexOf("SINSTI") == 0) { + if (checksum != checkLinky) { + StockMessage("Erreur checksum code : " + code + " " + String(checksum) + "," + String(checkLinky)); + } else { + if (code.indexOf("EAST") == 0) { + + OldWh = Energie_M_Soutiree; + if (OldWh == 0) { OldWh = val.toInt(); } + Energie_M_Soutiree = val.toInt(); + Tm = millis(); + deltaT = float(Tm - TlastEASTvalide); + deltaT = deltaT / float(3600000); + if (Energie_M_Soutiree == OldWh) { //Pas de resultat en Wh + Pmax = 1.3 / deltaT; + moyPWS = min(moyPWS, Pmax); + } else { + TlastEASTvalide = Tm; + deltaWh = float(Energie_M_Soutiree - OldWh); + deltaWS = deltaWh / deltaT; + Pmin = (deltaWh - 1) / deltaT; + moyPWS = max(moyPWS, Pmin); //saut à la montée en puissance + } + moyPWS = 0.05 * deltaWS + 0.95 * moyPWS; + EASTvalid = true; + if (!EAITvalid && Tm > 8000) { //Cas des CACSI ou EAIT n'est jamais positionné + EAITvalid = true; + } + } + if (code.indexOf("EAIT") == 0) { + OldWh = Energie_M_Injectee; + if (OldWh == 0) { OldWh = val.toInt(); } + Energie_M_Injectee = val.toInt(); + Tm = millis(); + deltaT = float(Tm - TlastEAITvalide); + deltaT = deltaT / float(3600000); + if (Energie_M_Injectee == OldWh) { //Pas de resultat en Wh + Pmax = 1.3 / deltaT; + moyPWI = min(moyPWI, Pmax); + } else { + TlastEAITvalide = Tm; + deltaWh = float(Energie_M_Injectee - OldWh); + deltaWI = deltaWh / deltaT; + Pmin = (deltaWh - 1) / deltaT; + moyPWI = max(moyPWI, Pmin); //saut à la montée en puissance + } + moyPWI = 0.05 * deltaWI + 0.95 * moyPWI; + EAITvalid = true; + } + if (EASTvalid && EAITvalid) { + EnergieActiveValide = true; + } + if (code == "SINSTS") { //Puissance apparente soutirée. Egalité pour ne pas confondre avec SINSTS1 (triphasé) + PVAS_M = PintMax(val.toInt()); + moyPVAS = 0.05 * float(PVAS_M) + 0.95 * moyPVAS; + moyPWS = min(moyPWS, moyPVAS); + if (moyPVAS > 0) { + COSphiS = moyPWS / moyPVAS; + COSphiS = min(float(1.0), COSphiS); + PowerFactor_M = COSphiS; + } + PuissanceS_M = PintMax(int(COSphiS * float(PVAS_M))); + Pva_valide=true; + } + if (code.indexOf("SINSTI") == 0) { //Puissance apparente injectée + PVAI_M = PintMax(val.toInt()); + moyPVAI = 0.05 * float(PVAI_M) + 0.95 * moyPVAI; + moyPWI = min(moyPWI, moyPVAI); + if (moyPVAI > 0) { + COSphiI = moyPWI / moyPVAI; + COSphiI = min(float(1.0), COSphiI); + PowerFactor_M = COSphiI; + } + PuissanceI_M = PintMax(int(COSphiI * float(PVAI_M))); + Pva_valide=true; + } + } + } + if (code.indexOf("DATE") == 0) { + ComOK(); //Reset du Watchdog à chaque trame du Linky reçue + } + if (code.indexOf("URMS1") == 0) { + Tension_M = val.toFloat(); //phase 1 uniquement + } + if (code.indexOf("IRMS1") == 0) { + Intensite_M = val.toFloat(); //Phase 1 uniquement + } + if (TempoEDFon == 0) { // On prend tarif sur Linky + if (code.indexOf("LTARF") == 0) { + LTARF = val; //Option Tarifaire + LTARF.trim(); + } + if (code.indexOf("STGE") == 0) { + STGE = val; //Status + STGE.trim(); + STGE = STGE.substring(1, 2); //Tempo lendemain et jour sur 1 octet + } + } + } + break; + default: + break; + } + } +} \ No newline at end of file diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/Source_MQTT.ino b/docs/routers/F1ATB/Solar_Router_V10.00/Source_MQTT.ino new file mode 100644 index 0000000..1aca8de --- /dev/null +++ b/docs/routers/F1ATB/Solar_Router_V10.00/Source_MQTT.ino @@ -0,0 +1,54 @@ + +// ****************************************************** +// * Informations de puissance reçue via un Broker MQTT * +// ****************************************************** +void UpdatePmqtt() { + float Pw = PfloatMax(PwMQTT); + float Pf = 1; + if (P_MQTT_Brute.indexOf("Pf") > 0) { + Pf = abs(PfMQTT); + } + if (P_MQTT_Brute.indexOf("Pva") > 0) { + if (PvaMQTT != 0) { + Pf = abs(Pw / PfloatMax(PvaMQTT)); + } + } + if (P_MQTT_Brute.indexOf("Pva") > 0 || P_MQTT_Brute.indexOf("Pf") > 0) { + Pva_valide = true; + } else { + Pva_valide = false; + } + if (Pf > 1) Pf = 1; + if (Pw >= 0) { + PuissanceS_M_inst = Pw; + PuissanceI_M_inst = 0; + if (Pf > 0.01) { + PVAS_M_inst = PfloatMax(Pw / Pf); + } else { + PVAS_M_inst = 0; + } + PVAI_M_inst = 0; + EASfloat += Pw / 6000; // Watt Hour,Every 600ms. Soutirée + Energie_M_Soutiree = int(EASfloat); // Watt Hour,Every 40ms. Soutirée + } else { + PuissanceS_M_inst = 0; + PuissanceI_M_inst = -Pw; + if (Pf > 0.01) { + PVAI_M_inst = PfloatMax(-Pw / Pf); + } else { + PVAI_M_inst = 0; + } + PVAS_M_inst = 0; + EAIfloat += -Pw / 6000; + Energie_M_Injectee = int(EAIfloat); + } + + filtre_puissance(); + + if (P_MQTT_Brute.indexOf("Pw") > 0) EnergieActiveValide = true; + if (millis() - LastPwMQTTMillis < 30000) ComOK(); //Reset du Watchdog si trame MQTT reçue avec au minimum Pw récente + + if (cptLEDyellow > 30) { + cptLEDyellow = 4; + } +} \ No newline at end of file diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/Source_ShellyEm.ino b/docs/routers/F1ATB/Solar_Router_V10.00/Source_ShellyEm.ino new file mode 100644 index 0000000..2444e0b --- /dev/null +++ b/docs/routers/F1ATB/Solar_Router_V10.00/Source_ShellyEm.ino @@ -0,0 +1,177 @@ +// **************************************************** +// * Client d'un Shelly Em sur voie 0 ou 1 ou triphasé* +// **************************************************** +void LectureShellyEm() { + String S = ""; + String Shelly_Data = ""; + float Pw = 0; + float voltage = 0; + float pf = 0; + + + // Use WiFiClient class to create TCP connections + WiFiClient clientESP_RMS; + byte arr[4]; + arr[0] = RMSextIP & 0xFF; // 0x78 + arr[1] = (RMSextIP >> 8) & 0xFF; // 0x56 + arr[2] = (RMSextIP >> 16) & 0xFF; // 0x34 + arr[3] = (RMSextIP >> 24) & 0xFF; // 0x12 + + String host = String(arr[3]) + "." + String(arr[2]) + "." + String(arr[1]) + "." + String(arr[0]); + if (!clientESP_RMS.connect(host.c_str(), 80)) { + StockMessage("connection to Shelly Em failed : " + host); + delay(200); + ComAbuge(); + return; + } + int voie = EnphaseSerial.toInt(); + int Voie = voie % 2; + + if (ShEm_comptage_appels == 1) { + Voie = (Voie + 1) % 2; + } + String url = "/emeter/" + String(Voie); + if (voie == 3) url = "/status"; //Triphasé + ShEm_comptage_appels = (ShEm_comptage_appels + 1) % 5; // 1 appel sur 6 vers la deuxième voie qui ne sert pas au routeur + clientESP_RMS.print(String("GET ") + url + " HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Connection: close\r\n\r\n"); + unsigned long timeout = millis(); + while (clientESP_RMS.available() == 0) { + if (millis() - timeout > 5000) { + StockMessage("client Shelly Em Timeout ! : " + host); + clientESP_RMS.stop(); + return; + } + } + timeout = millis(); + // Lecture des données brutes distantes + while (clientESP_RMS.available() && (millis() - timeout < 5000)) { + Shelly_Data += clientESP_RMS.readStringUntil('\r'); + } + int p = Shelly_Data.indexOf("{"); + Shelly_Data = Shelly_Data.substring(p); + if (voie == 3) { //Triphasé + ShEm_dataBrute = "Triphasé
" + Shelly_Data; + p = Shelly_Data.indexOf("emeters"); + Shelly_Data = Shelly_Data.substring(p + 10); + Pw = PfloatMax(ValJson("power", Shelly_Data)); //Phase 1 + pf = ValJson("pf", Shelly_Data); + pf = abs(pf); + float total_Pw = Pw; + float total_Pva = 0; + if (pf > 0) { + total_Pva = abs(Pw) / pf; + } + float total_E_soutire = ValJson("total\"", Shelly_Data); + float total_E_injecte = ValJson("total_returned", Shelly_Data); + p = Shelly_Data.indexOf("}"); + Shelly_Data = Shelly_Data.substring(p + 1); + Pw = PfloatMax(ValJson("power", Shelly_Data)); //Phase 2 + pf = ValJson("pf", Shelly_Data); + pf = abs(pf); + total_Pw += Pw; + if (pf > 0) { + total_Pva += abs(Pw) / pf; + } + total_E_soutire += ValJson("total\"", Shelly_Data); + total_E_injecte += ValJson("total_returned", Shelly_Data); + p = Shelly_Data.indexOf("}"); + Shelly_Data = Shelly_Data.substring(p + 1); + Pw = PfloatMax(ValJson("power", Shelly_Data)); //Phase 3 + pf = ValJson("pf", Shelly_Data); + pf = abs(pf); + total_Pw += Pw; + if (pf > 0) { + total_Pva += abs(Pw) / pf; + } + total_E_soutire += ValJson("total\"", Shelly_Data); + total_E_injecte += ValJson("total_returned", Shelly_Data); + Energie_M_Soutiree = int(total_E_soutire); + Energie_M_Injectee = int(total_E_injecte); + if (total_Pw == 0) { + total_Pva = 0; + } + if (total_Pw > 0) { + PuissanceS_M_inst = total_Pw; + PuissanceI_M_inst = 0; + PVAS_M_inst = total_Pva; + PVAI_M_inst = 0; + } else { + PuissanceS_M_inst = 0; + PuissanceI_M_inst = -total_Pw; + PVAI_M_inst = total_Pva; + PVAS_M_inst = 0; + } + } else { //Monophasé + ShEm_dataBrute = "Voie : " + String(voie) + "
" + Shelly_Data; + Shelly_Data = Shelly_Data + ","; + if (Shelly_Data.indexOf("true") > 0) { // Donnée valide + Pw = PfloatMax(ValJson("power", Shelly_Data)); + voltage = ValJson("voltage", Shelly_Data); + pf = ValJson("pf", Shelly_Data); + pf = abs(pf); + if (pf > 1) pf = 1; + if (Voie == voie) { //voie du routeur + if (Pw >= 0) { + PuissanceS_M_inst = Pw; + PuissanceI_M_inst = 0; + if (pf > 0.01) { + PVAS_M_inst = PfloatMax(Pw / pf); + } else { + PVAS_M_inst = 0; + } + PVAI_M_inst = 0; + } else { + PuissanceS_M_inst = 0; + PuissanceI_M_inst = -Pw; + if (pf > 0.01) { + PVAI_M_inst = PfloatMax(-Pw / pf); + } else { + PVAI_M_inst = 0; + } + PVAS_M_inst = 0; + } + Energie_M_Soutiree = int(ValJson("total\"", Shelly_Data)); + Energie_M_Injectee = int(ValJson("total_returned", Shelly_Data)); + PowerFactor_M = pf; + Tension_M = voltage; + Pva_valide=true; + } else { // voie secondaire + if (LissageLong) { + PwMoy2 = 0.2 * Pw + 0.8 * PwMoy2; //Lissage car moins de mesure sur voie secondaire + pfMoy2 = 0.2 * pf + 0.8 * pfMoy2; + Pw = PwMoy2; + pf = pfMoy2; + } + if (Pw >= 0) { + PuissanceS_T_inst = Pw; + PuissanceI_T_inst = 0; + if (pf > 0.01) { + PVAS_T_inst = PfloatMax(Pw / pf); + } else { + PVAS_T_inst = 0; + } + PVAI_T_inst = 0; + } else { + PuissanceS_T_inst = 0; + PuissanceI_T_inst = -Pw; + if (pf > 0.01) { + PVAI_T_inst = PfloatMax(-Pw / pf); + } else { + PVAI_T_inst = 0; + } + PVAS_T_inst = 0; + } + Energie_T_Soutiree = int(ValJson("total\"", Shelly_Data)); + Energie_T_Injectee = int(ValJson("total_returned", Shelly_Data)); + PowerFactor_T = pf; + Tension_T = voltage; + } + } + } + filtre_puissance(); + ComOK(); //Reset du Watchdog à chaque trame du Shelly reçue + if (ShEm_comptage_appels > 1) EnergieActiveValide = true; + if (cptLEDyellow > 30) { + cptLEDyellow = 4; + } +} diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/Source_SmartG.ino b/docs/routers/F1ATB/Solar_Router_V10.00/Source_SmartG.ino new file mode 100644 index 0000000..b3ced44 --- /dev/null +++ b/docs/routers/F1ATB/Solar_Router_V10.00/Source_SmartG.ino @@ -0,0 +1,75 @@ +// ****************************** +// * Client d'un Smart Gateways * +// ****************************** +void LectureSmartG() { + String S = ""; + String SmartG_Data = ""; + String Gr[4]; + String data_[20]; + + Pva_valide=false; + // Use WiFiClient class to create TCP connections + WiFiClient clientESP_RMS; + byte arr[4]; + arr[0] = RMSextIP & 0xFF; // 0x78 + arr[1] = (RMSextIP >> 8) & 0xFF; // 0x56 + arr[2] = (RMSextIP >> 16) & 0xFF; // 0x34 + arr[3] = (RMSextIP >> 24) & 0xFF; // 0x12 + + String host = String(arr[3]) + "." + String(arr[2]) + "." + String(arr[1]) + "." + String(arr[0]); + if (!clientESP_RMS.connect(host.c_str(), 82)) { // PORT 82 pour Smlart Gateways + StockMessage("connection to SmartGateways failed : " + host); + delay(200); + ComAbuge(); + return; + } + String url = "/smartmeter/api/read"; + clientESP_RMS.print(String("GET ") + url + " HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Connection: close\r\n\r\n"); + unsigned long timeout = millis(); + while (clientESP_RMS.available() == 0) { + if (millis() - timeout > 5000) { + StockMessage(">>> client SmartGateways Timeout ! : " + host); + clientESP_RMS.stop(); + return; + } + } + timeout = millis(); + // Lecture des données brutes distantes + while (clientESP_RMS.available() && (millis() - timeout < 5000)) { + SmartG_Data += clientESP_RMS.readStringUntil('\r'); + } + int p = SmartG_Data.indexOf("{"); + SmartG_Data = SmartG_Data.substring(p+1); + p = SmartG_Data.indexOf("}"); + SmartG_Data = SmartG_Data.substring(0,p); + PuissanceS_M_inst=PfloatMax(ValJsonSG("PowerDelivered_total", SmartG_Data)); + PuissanceI_M_inst=PfloatMax(ValJsonSG("PowerReturned_total", SmartG_Data)); + long EnergyDeliveredTariff1=int(1000*ValJsonSG("EnergyDeliveredTariff1", SmartG_Data)); + long EnergyDeliveredTariff2=int(1000*ValJsonSG("EnergyDeliveredTariff2", SmartG_Data)); + Energie_M_Soutiree=EnergyDeliveredTariff1+EnergyDeliveredTariff2; + long EnergyReturnedTariff1=int(1000*ValJsonSG("EnergyReturnedTariff1", SmartG_Data)); + long EnergyReturnedTariff2=int(1000*ValJsonSG("EnergyReturnedTariff2", SmartG_Data)); + Energie_M_Injectee=EnergyReturnedTariff1+EnergyReturnedTariff2; + SG_dataBrute=SmartG_Data; + filtre_puissance(); + ComOK(); //Reset du Watchdog à chaque trame du SmartGateways reçue + EnergieActiveValide = true; + + if (cptLEDyellow > 30) { + cptLEDyellow = 4; + } +} + +float ValJsonSG(String nom, String Json) { + int p = Json.indexOf(nom); + Json = Json.substring(p); + p = Json.indexOf(":"); + Json = Json.substring(p + 2); + p = Json.indexOf(","); + float val = 0; + if (p > 0) { + Json = Json.substring(0, p); + val = Json.toFloat(); + } + return val; +} diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/Source_UxI.ino b/docs/routers/F1ATB/Solar_Router_V10.00/Source_UxI.ino new file mode 100644 index 0000000..28191b5 --- /dev/null +++ b/docs/routers/F1ATB/Solar_Router_V10.00/Source_UxI.ino @@ -0,0 +1,76 @@ +// **************************** +// * Source de Mesures U et I * +// * UXI * +// **************************** + + +void Setup_UxI() { + for (int i = 0; i < 100; i++) { //Reset table measurements + voltM[i] = 0; + ampM[i] = 0; + } +} +void LectureUxI() { + MeasurePower(); + ComputePower(); +} +void MeasurePower() { //Lecture Tension et courants pendant 20ms + int iStore; + value0 = analogRead(AnalogIn0); //Mean value. Should be at 3.3v/2 + unsigned long MeasureMillis = millis(); + + while (millis() - MeasureMillis < 21) { //Read values in continuous during 20ms. One loop is around 150 micro seconds + iStore = (micros() % 20000) / 200; //We have more results that we need during 20ms to fill the tables of 100 samples + volt[iStore] = analogRead(AnalogIn1) - value0; + amp[iStore] = analogRead(AnalogIn2) - value0; + } +} +void ComputePower() { + float PWcal = 0; //Computation Power in Watt + float V; + float I; + float Uef2 = 0; + float Ief2 = 0; + for (int i = 0; i < 100; i++) { + voltM[i] = (19 * voltM[i] + float(volt[i])) / 20; //Mean value. First Order Filter. Short Integration + V = kV * voltM[i]; + Uef2 += sq(V); + ampM[i] = (19 * ampM[i] + float(amp[i])) / 20; //Mean value. First Order Filter + I = kI * ampM[i]; + Ief2 += sq(I); + PWcal += V * I; + } + Uef2 = Uef2 / 100; //square of voltage + Tension_M = sqrt(Uef2); //RMS voltage + Ief2 = Ief2 / 100; //square of current + Intensite_M = sqrt(Ief2); // RMS current + PWcal = PfloatMax(PWcal / 100); + float PVA =PfloatMax( floor(Tension_M * Intensite_M)); + float PowerFactor = 0; + if (PVA > 0) { + PowerFactor = floor(100 * PWcal / PVA) / 100; + } + PowerFactor_M = PowerFactor; + if (PWcal >= 0) { + EASfloat += PWcal / 90000; // Watt Hour,Every 40ms. Soutirée + Energie_M_Soutiree =int(EASfloat); // Watt Hour,Every 40ms. Soutirée + PuissanceS_M_inst = PWcal; + PuissanceI_M_inst = 0; + PVAS_M_inst = PVA; + PVAI_M_inst = 0; + } else { + EAIfloat += -PWcal / 90000; + Energie_M_Injectee =int(EAIfloat); + PuissanceS_M_inst = 0; + PuissanceI_M_inst = -PWcal; + PVAS_M_inst = 0; + PVAI_M_inst = PVA; + } + Pva_valide=true; + if (cptLEDyellow > 30) { + cptLEDyellow = 4; + } + filtre_puissance(); + EnergieActiveValide = true; + ComOK(); +} diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/Source_UxIx2.ino b/docs/routers/F1ATB/Solar_Router_V10.00/Source_UxIx2.ino new file mode 100644 index 0000000..8dc33d1 --- /dev/null +++ b/docs/routers/F1ATB/Solar_Router_V10.00/Source_UxIx2.ino @@ -0,0 +1,96 @@ +// ******************************* +// * Source de Mesures UI Double * +// * Capteur JSY-MK-194 * +// ******************************* + +void Setup_UxIx2() { + MySerial.setRxBufferSize(SER_BUF_SIZE); + MySerial.begin(4800, SERIAL_8N1, RXD2, TXD2); //PORT DE CONNEXION AVEC LE CAPTEUR JSY-MK-194 +} +void LectureUxIx2() { //Ecriture et Lecture port série du JSY-MK-194 . + + int i, j; + byte msg_send[] = { 0x01, 0x03, 0x00, 0x48, 0x00, 0x0E, 0x44, 0x18 }; + // Demande Info sur le Serial port 2 (Modbus RTU) + for (i = 0; i < 8; i++) { + MySerial.write(msg_send[i]); + } + + //Réponse en général à l'appel précédent (seulement 4800bauds) + int a = 0; + while (MySerial.available()) { + ByteArray[a] = MySerial.read(); + a++; + } + + + if (a == 61) { //Message complet reçu + j = 3; + for (i = 0; i < 14; i++) { // conversion séries de 4 octets en long + LesDatas[i] = 0; + LesDatas[i] += ByteArray[j] << 24; + j += 1; + LesDatas[i] += ByteArray[j] << 16; + j += 1; + LesDatas[i] += ByteArray[j] << 8; + j += 1; + LesDatas[i] += ByteArray[j]; + j += 1; + } + Sens_1 = ByteArray[27]; // Sens 1 + Sens_2 = ByteArray[28]; + + //Données du Triac + Tension_T = LesDatas[0] * .0001; + Intensite_T = LesDatas[1] * .0001; + float Puiss_1 = PfloatMax(LesDatas[2] * .0001); + Energie_T_Soutiree = int(LesDatas[3] * .1); + PowerFactor_T = LesDatas[4] * .001; + Energie_T_Injectee = int(LesDatas[5] * .1); + Frequence = LesDatas[7] * .01; + float PVA1 = 0; + if (PowerFactor_T > 0) { + PVA1 = Puiss_1 / PowerFactor_T; + } + if (Sens_1 > 0) { //Injection sur TRiac. Ne devrait pas arriver + PuissanceI_T_inst = Puiss_1; + PuissanceS_T_inst = 0; + PVAI_T_inst = PVA1; + PVAS_T_inst = 0; + } else { + PuissanceS_T_inst = Puiss_1; + PuissanceI_T_inst = 0; + PVAI_T_inst = 0; + PVAS_T_inst = PVA1; + } + // Données générale de la Maison + Tension_M = LesDatas[8] * .0001; + Intensite_M = LesDatas[9] * .0001; + float Puiss_2 = PfloatMax(LesDatas[10] * .0001); + Energie_M_Soutiree = int(LesDatas[11] * .1); + PowerFactor_M = LesDatas[12] * .001; + Energie_M_Injectee = int(LesDatas[13] * .1); + float PVA2 = 0; + if (PowerFactor_M > 0) { + PVA2 = Puiss_2 / PowerFactor_M; + } + if (Sens_2 > 0) { //Injection en entrée de Maison + PuissanceI_M_inst = Puiss_2; + PuissanceS_M_inst = 0; + PVAI_M_inst = PVA2; + PVAS_M_inst = 0; + } else { + PuissanceS_M_inst = Puiss_2; + PuissanceI_M_inst = 0; + PVAI_M_inst = 0; + PVAS_M_inst = PVA2; + } + filtre_puissance(); + EnergieActiveValide = true; + Pva_valide = true; + ComOK(); //Reset du Watchdog à chaque trame du module JSY-MK-194 reçue + if (cptLEDyellow > 30) { + cptLEDyellow = 4; + } + } +} \ No newline at end of file diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/Source_UxIx3.ino b/docs/routers/F1ATB/Solar_Router_V10.00/Source_UxIx3.ino new file mode 100644 index 0000000..a1e08a5 --- /dev/null +++ b/docs/routers/F1ATB/Solar_Router_V10.00/Source_UxIx3.ino @@ -0,0 +1,125 @@ +// ************************************************* +// * Client lecture JSY-MK-333 * Triphasé * +// * Développement initial de Pierre F (Mars 2024) * +// * update PhDV61 Juin 2024 * +// ************************************************* + + +void Setup_JSY333() { + MySerial.setRxBufferSize(SER_BUF_SIZE); + MySerial.begin(9600, SERIAL_8N1, RXD2, TXD2); //PORT DE CONNEXION AVEC LE CAPTEUR JSY-MK-333 +} + +void Lecture_JSY333() { + float Tension_M1, Tension_M2, Tension_M3; + float Intensite_M1, Intensite_M2, Intensite_M3; + float PVA_M_inst1, PVA_M_inst2, PVA_M_inst3; + float PW_inst1, PW_inst2, PW_inst3; + + byte Lecture333[200]; + bool injection; + bool sens1, sens2, sens3; + long delta_temps = 0; + + int i; + byte msg_send[] = { 0x01, 0x03, 0x01, 0x00, 0x00, 0x44, 0x44, 0x05 }; + for (i = 0; i < 8; i++) { + MySerial.write(msg_send[i]); + } + + int a = 0; + while (MySerial.available()) { + Lecture333[a] = MySerial.read(); + a++; + } + + if (a == 141) { //message complet reçu + delta_temps = (unsigned long)(millis() - Temps_precedent); // temps écoulé depuis le dernier appel + Temps_precedent = millis(); // on conserve la valeur du temps actuel pour le calcul précédent + + Tension_M1 = ((float)(Lecture333[3] * 256 + Lecture333[4])) / 100; + Tension_M2 = ((float)(Lecture333[5] * 256 + Lecture333[6])) / 100; + Tension_M3 = ((float)(Lecture333[7] * 256 + Lecture333[8])) / 100; + Intensite_M1 = ((float)(Lecture333[9] * 256 + Lecture333[10])) / 100; + Intensite_M2 = ((float)(Lecture333[11] * 256 + Lecture333[12])) / 100; + Intensite_M3 = ((float)(Lecture333[13] * 256 + Lecture333[14])) / 100; + + sens1 = (Lecture333[104]) & 0x01; + sens2 = (Lecture333[104] >> 1) & 0x01; + sens3 = (Lecture333[104] >> 2) & 0x01; + + if (sens1) { Intensite_M1 *= -1; } + if (sens2) { Intensite_M2 *= -1; } + if (sens3) { Intensite_M3 *= -1; } + + injection = (Lecture333[104] >> 3) & 0x01; //si sens est true, injection + + // Lecture des Puissances actives de chacune des phases + PW_inst1 = (float)(Lecture333[15] * 256.0) + (float)Lecture333[16]; + PW_inst2 = (float)(Lecture333[17] * 256.0) + (float)Lecture333[18]; + PW_inst3 = (float)(Lecture333[19] * 256.0) + (float)Lecture333[20]; + + //Lecture des puissances apparentes de chacune des phases, qu'on signe comme le Linky + PVA_M_inst1 = (float)(Lecture333[35] * 256) + (float)Lecture333[36]; + if (sens1) { PVA_M_inst1 = -PVA_M_inst1; } + PVA_M_inst2 = (float)(Lecture333[37] * 256) + (float)Lecture333[38]; + if (sens2) { PVA_M_inst2 = -PVA_M_inst2; } + PVA_M_inst3 = (float)(Lecture333[39] * 256) + (float)Lecture333[40]; + if (sens3) { PVA_M_inst3 = -PVA_M_inst3; } + + if (injection) { + PuissanceS_M_inst = 0; + PuissanceI_M_inst = ((float)((float)(Lecture333[21] * 16777216) + (float)(Lecture333[22] * 65536) + (float)(Lecture333[23] * 256) + (float)Lecture333[24])); + PVAS_M_inst = 0; + PVAI_M_inst = abs(PVA_M_inst1 + PVA_M_inst2 + PVA_M_inst3); // car la somme des puissances apparentes "signées" est négative puisqu'en "injection" au global + + // PhDV61 : on considère que cette puissance active "globale" a duré "delta_temps", et on l'intègre donc pour obtenir une énergie en Wh + Energie_jour_Injectee += ((float)delta_temps / 1000) * (PuissanceI_M_inst / 3600.0); + + } else { // soutirage + PuissanceI_M_inst = 0; + PuissanceS_M_inst = ((float)((float)(Lecture333[21] * 16777216) + (float)(Lecture333[22] * 65536) + (float)(Lecture333[23] * 256) + (float)Lecture333[24])); + PVAI_M_inst = 0; + PVAS_M_inst = PVA_M_inst1 + PVA_M_inst2 + PVA_M_inst3; + + // PhDV61 : on considère que cette puissance active "globale" a duré "delta_temps", et on l'intègre donc pour obtenir pour obtenir une énergie en Wh + Energie_jour_Soutiree += ((float)delta_temps / 1000) * (PuissanceS_M_inst / 3600.0); + } + + // PowerFactor_M = ((float)(Lecture333[53] * 256 + Lecture333[54])) / 1000; ce facteur de puissance ne veut rien dire en tri-phasé en cas d'injection sur au moins une phase + + Energie_M_Soutiree = ((float)((float)(Lecture333[119] * 16777216) + (float)(Lecture333[120] * 65536) + (float)(Lecture333[121] * 256) + (float)Lecture333[122])) * 10; + Energie_M_Injectee = ((float)((float)(Lecture333[135] * 16777216) + (float)(Lecture333[136] * 65536) + (float)(Lecture333[137] * 256) + (float)Lecture333[138])) * 10; + + MK333_dataBrute = "Triphasé
Phase1 : " + String(int(Tension_M1)) + "V " + String(Intensite_M1) + "A
"; + MK333_dataBrute += "
Phase2 : " + String(int(Tension_M2)) + "V " + String(Intensite_M2) + "A
"; + MK333_dataBrute += "
Phase3 : " + String(int(Tension_M3)) + "V " + String(Intensite_M3) + "A
"; + MK333_dataBrute += "
Puissance active soutirée : " + String(PuissanceS_M_inst) + "W
"; + MK333_dataBrute += "
Puissance active injectée : " + String(PuissanceI_M_inst) + "W
"; + MK333_dataBrute += "
Puissance apparente soutirée : " + String(PVAS_M_inst) + "VA
"; + MK333_dataBrute += "
Puissance apparente injectée : " + String(PVAI_M_inst) + "VA
"; + + if (PVA_M_inst1 != 0) + MK333_dataBrute += "
Facteur de puissance phase 1 : " + String(abs(PW_inst1 / PVA_M_inst1)) + "
"; + if (PVA_M_inst2 != 0) + MK333_dataBrute += "
Facteur de puissance phase 2 : " + String(abs(PW_inst2 / PVA_M_inst2)) + "
"; + if (PVA_M_inst3 != 0) + MK333_dataBrute += "
Facteur de puissance phase 3 : " + String(abs(PW_inst3 / PVA_M_inst3)) + "
"; + + MK333_dataBrute += "
Energie jour nette soutirée (Linky): " + String(Energie_jour_Soutiree) + "Wh
"; + MK333_dataBrute += "
Energie jour nette injectée (Linky): " + String(Energie_jour_Injectee) + "Wh
"; + + MK333_dataBrute += "
Energie totale soutirée : " + String(Energie_M_Soutiree) + "Wh
"; + MK333_dataBrute += "
Energie totale injectée : " + String(Energie_M_Injectee) + "Wh
"; + + Pva_valide = true; + filtre_puissance(); + ComOK(); //Reset du Watchdog à chaque trame du JSY reçue + EnergieActiveValide = true; + if (cptLEDyellow > 30) { + cptLEDyellow = 4; + } + } else { + StockMessage("Pas tout reçu, pas traité... nombre de données : " + String(a)); + } +} \ No newline at end of file diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/Stockage.ino b/docs/routers/F1ATB/Solar_Router_V10.00/Stockage.ino new file mode 100644 index 0000000..5c0b1a0 --- /dev/null +++ b/docs/routers/F1ATB/Solar_Router_V10.00/Stockage.ino @@ -0,0 +1,466 @@ +// *************************** +// Stockage des données en ROM +// *************************** +//Plan stockage +#define EEPROM_SIZE 4090 +#define NbJour 370 //Nb jour historique stocké +#define adr_HistoAn 0 //taille 2* 370*4=1480 +#define adr_E_T_soutire0 1480 // 1 long. Taille 4 Triac +#define adr_E_T_injecte0 1484 +#define adr_E_M_soutire0 1488 // 1 long. Taille 4 Maison +#define adr_E_M_injecte0 1492 // 1 long. Taille 4 +#define adr_DateCeJour 1496 // String 8+1 +#define adr_lastStockConso 1505 // Short taille 2 +#define adr_ParaActions 1507 //Clé + ensemble parametres peu souvent modifiés + + +void INIT_EEPROM(void) { + if (!EEPROM.begin(EEPROM_SIZE)) { + StockMessage("Failed to initialise EEPROM"); + delay(10000); + ESP.restart(); + } +} + +void RAZ_Histo_Conso() { + //Mise a zero Zone stockage + int Adr_SoutInjec = adr_HistoAn; + for (int i = 0; i < NbJour; i++) { + EEPROM.writeLong(Adr_SoutInjec, 0); + Adr_SoutInjec = Adr_SoutInjec + 4; + } + EEPROM.writeULong(adr_E_T_soutire0, 0); + EEPROM.writeULong(adr_E_T_injecte0, 0); + EEPROM.writeULong(adr_E_M_soutire0, 0); + EEPROM.writeULong(adr_E_M_injecte0, 0); + EEPROM.writeString(adr_DateCeJour, ""); + EEPROM.writeUShort(adr_lastStockConso, 0); + EEPROM.commit(); +} + +void LectureConsoMatinJour(void) { + + Energie_jour_Soutiree = 0; // en Wh + Energie_jour_Injectee = 0; // en Wh + + EAS_T_J0 = EEPROM.readULong(adr_E_T_soutire0); //Triac + EAI_T_J0 = EEPROM.readULong(adr_E_T_injecte0); + EAS_M_J0 = EEPROM.readULong(adr_E_M_soutire0); //Maison + EAI_M_J0 = EEPROM.readULong(adr_E_M_injecte0); + DateCeJour = EEPROM.readString(adr_DateCeJour); + idxPromDuJour = EEPROM.readUShort(adr_lastStockConso); + if (Energie_T_Soutiree < EAS_T_J0) { + Energie_T_Soutiree = EAS_T_J0; + } + if (Energie_T_Injectee < EAI_T_J0) { + Energie_T_Injectee = EAI_T_J0; + } + if (Energie_M_Soutiree < EAS_M_J0) { + Energie_M_Soutiree = EAS_M_J0; + } + if (Energie_M_Injectee < EAI_M_J0) { + Energie_M_Injectee = EAI_M_J0; + } +} + + +void JourHeureChange() { + if (DATEvalid) { + //Time Update / de l'heure + time_t timestamp = time(NULL); + char buffer[MAX_SIZE_T]; + struct tm *pTime = localtime(×tamp); + strftime(buffer, MAX_SIZE_T, "%d/%m/%Y %H:%M:%S", pTime); + DATE = String(buffer); + strftime(buffer, MAX_SIZE_T, "%d%m%Y", pTime); + String JourCourant = String(buffer); + strftime(buffer, MAX_SIZE_T, "%Y-%m-%d", pTime); + DateEDF = String(buffer); + strftime(buffer, MAX_SIZE_T, "%H", pTime); + int hour = String(buffer).toInt(); + strftime(buffer, MAX_SIZE_T, "%M", pTime); + int minute = String(buffer).toInt(); + strftime(buffer, MAX_SIZE_T, "%s", pTime); + unsigned long Tactu = String(buffer).toInt(); + if (T0_seconde == 0) T0_seconde = Tactu; + T_On_seconde = Tactu - T0_seconde; + HeureCouranteDeci = hour * 100 + minute * 10 / 6; + if (DateCeJour != JourCourant) { //Changement de jour + if (EnergieActiveValide && DateCeJour != "") { //Données recues + idxPromDuJour = (idxPromDuJour + 1 + NbJour) % NbJour; + //On enregistre les conso en début de journée pour l'historique de l'année + long energie = Energie_M_Soutiree - Energie_M_Injectee; //Bilan energie du jour + EEPROM.writeLong(idxPromDuJour * 4, energie); + EEPROM.writeULong(adr_E_T_soutire0, long(Energie_T_Soutiree)); + EEPROM.writeULong(adr_E_T_injecte0, long(Energie_T_Injectee)); + EEPROM.writeULong(adr_E_M_soutire0, long(Energie_M_Soutiree)); + EEPROM.writeULong(adr_E_M_injecte0, long(Energie_M_Injectee)); + EEPROM.writeString(adr_DateCeJour, JourCourant); + EEPROM.writeUShort(adr_lastStockConso, idxPromDuJour); + EEPROM.commit(); + LectureConsoMatinJour(); + } + DateCeJour = JourCourant; + } + } +} +String HistoriqueEnergie1An(void) { + String S = ""; + int Adr_SoutInjec = 0; + long EnergieJour = 0; + long DeltaEnergieJour = 0; + int iS = 0; + long lastDay = 0; + + for (int i = 0; i < NbJour; i++) { + iS = (idxPromDuJour + i + 1) % NbJour; + Adr_SoutInjec = adr_HistoAn + iS * 4; + EnergieJour = EEPROM.readLong(Adr_SoutInjec); + if (lastDay == 0) { lastDay = EnergieJour; } + DeltaEnergieJour = EnergieJour - lastDay; + lastDay = EnergieJour; + S += String(DeltaEnergieJour) + ","; + } + return S; +} +unsigned long LectureCle() { + return EEPROM.readULong(adr_ParaActions); +} +void LectureEnROM() { + int Hdeb; + int address = adr_ParaActions; + int VersionStocke; + Cle_ROM = EEPROM.readULong(address); + address += sizeof(unsigned long); + VersionStocke = EEPROM.readUShort(address); + address += sizeof(unsigned short); + ssid = EEPROM.readString(address); + address += ssid.length() + 1; + password = EEPROM.readString(address); + address += password.length() + 1; + dhcpOn = EEPROM.readByte(address); + address += sizeof(byte); + IP_Fixe = EEPROM.readULong(address); + address += sizeof(unsigned long); + Gateway = EEPROM.readULong(address); + address += sizeof(unsigned long); + masque = EEPROM.readULong(address); + address += sizeof(unsigned long); + dns = EEPROM.readULong(address); + address += sizeof(unsigned long); + Source = EEPROM.readString(address); + address += Source.length() + 1; + RMSextIP = EEPROM.readULong(address); + address += sizeof(unsigned long); + EnphaseUser = EEPROM.readString(address); + address += EnphaseUser.length() + 1; + EnphasePwd = EEPROM.readString(address); + address += EnphasePwd.length() + 1; + EnphaseSerial = EEPROM.readString(address); + address += EnphaseSerial.length() + 1; + MQTTRepet = EEPROM.readUShort(address); + address += sizeof(unsigned short); + MQTTIP = EEPROM.readULong(address); + address += sizeof(unsigned long); + MQTTPort = EEPROM.readUShort(address); + address += sizeof(unsigned short); + MQTTUser = EEPROM.readString(address); + address += MQTTUser.length() + 1; + MQTTPwd = EEPROM.readString(address); + address += MQTTPwd.length() + 1; + MQTTPrefix = EEPROM.readString(address); + address += MQTTPrefix.length() + 1; + MQTTdeviceName = EEPROM.readString(address); + address += MQTTdeviceName.length() + 1; + TopicP = EEPROM.readString(address); + address += TopicP.length() + 1; + TopicT = EEPROM.readString(address); + address += TopicT.length() + 1; + subMQTT = EEPROM.readByte(address); + address += sizeof(byte); + nomRouteur = EEPROM.readString(address); + address += nomRouteur.length() + 1; + nomSondeFixe = EEPROM.readString(address); + address += nomSondeFixe.length() + 1; + nomSondeMobile = EEPROM.readString(address); + address += nomSondeMobile.length() + 1; + nomTemperature = EEPROM.readString(address); + address += nomTemperature.length() + 1; + Source_Temp = EEPROM.readString(address); + address += Source_Temp.length() + 1; + IPtemp = EEPROM.readULong(address); + address += sizeof(unsigned long); + CalibU = EEPROM.readUShort(address); + address += sizeof(unsigned short); + CalibI = EEPROM.readUShort(address); + address += sizeof(unsigned short); + TempoEDFon = EEPROM.readByte(address); + address += sizeof(byte); + WifiSleep = EEPROM.readByte(address); + address += sizeof(byte); + pSerial = EEPROM.readByte(address); + address += sizeof(byte); + pTriac = EEPROM.readByte(address); + address += sizeof(byte); + + address += 100; //Réserve de 100 bytes + + //Zone des actions + NbActions = EEPROM.readUShort(address); + address += sizeof(unsigned short); + for (int iAct = 0; iAct < NbActions; iAct++) { + LesActions[iAct].Actif = EEPROM.readByte(address); + address += sizeof(byte); + LesActions[iAct].Titre = EEPROM.readString(address); + address += LesActions[iAct].Titre.length() + 1; + LesActions[iAct].Host = EEPROM.readString(address); + address += LesActions[iAct].Host.length() + 1; + LesActions[iAct].Port = EEPROM.readUShort(address); + address += sizeof(unsigned short); + LesActions[iAct].OrdreOn = EEPROM.readString(address); + address += LesActions[iAct].OrdreOn.length() + 1; + LesActions[iAct].OrdreOff = EEPROM.readString(address); + address += LesActions[iAct].OrdreOff.length() + 1; + LesActions[iAct].Repet = EEPROM.readUShort(address); + address += sizeof(unsigned short); + LesActions[iAct].Tempo = EEPROM.readUShort(address); + address += sizeof(unsigned short); + LesActions[iAct].Reactivite = EEPROM.readByte(address); + address += sizeof(byte); + address += 40; //Réserve de 40 bytes + LesActions[iAct].NbPeriode = EEPROM.readByte(address); + address += sizeof(byte); + Hdeb = 0; + for (byte i = 0; i < LesActions[iAct].NbPeriode; i++) { + LesActions[iAct].Type[i] = EEPROM.readByte(address); + address += sizeof(byte); + LesActions[iAct].Hfin[i] = EEPROM.readUShort(address); + LesActions[iAct].Hdeb[i] = Hdeb; + Hdeb = LesActions[iAct].Hfin[i]; + address += sizeof(unsigned short); + LesActions[iAct].Vmin[i] = EEPROM.readShort(address); + address += sizeof(unsigned short); + LesActions[iAct].Vmax[i] = EEPROM.readShort(address); + address += sizeof(unsigned short); + LesActions[iAct].Tinf[i] = EEPROM.readShort(address); + address += sizeof(unsigned short); + LesActions[iAct].Tsup[i] = EEPROM.readShort(address); + address += sizeof(unsigned short); + LesActions[iAct].Tarif[i] = EEPROM.readByte(address); + address += sizeof(byte); + address += 10; //Réserve de 10 bytes + } + } + Calibration(address); + +} +int EcritureEnROM() { + int address = adr_ParaActions; + int VersionStocke = 0; + String V=Version; + VersionStocke =int(100*V.toFloat()); + EEPROM.writeULong(address, Cle_ROM); + address += sizeof(unsigned long); + EEPROM.writeUShort(address, VersionStocke); + address += sizeof(unsigned short); + EEPROM.writeString(address, ssid); + address += ssid.length() + 1; + EEPROM.writeString(address, password); + address += password.length() + 1; + EEPROM.writeByte(address, dhcpOn); + address += sizeof(byte); + EEPROM.writeULong(address, IP_Fixe); + address += sizeof(unsigned long); + EEPROM.writeULong(address, Gateway); + address += sizeof(unsigned long); + EEPROM.writeULong(address, masque); + address += sizeof(unsigned long); + EEPROM.writeULong(address, dns); + address += sizeof(unsigned long); + EEPROM.writeString(address, Source); + address += Source.length() + 1; + EEPROM.writeULong(address, RMSextIP); + address += sizeof(unsigned long); + EEPROM.writeString(address, EnphaseUser); + address += EnphaseUser.length() + 1; + EEPROM.writeString(address, EnphasePwd); + address += EnphasePwd.length() + 1; + EEPROM.writeString(address, EnphaseSerial); + address += EnphaseSerial.length() + 1; + EEPROM.writeUShort(address, MQTTRepet); + address += sizeof(unsigned short); + EEPROM.writeULong(address, MQTTIP); + address += sizeof(unsigned long); + EEPROM.writeUShort(address, MQTTPort); + address += sizeof(unsigned short); + EEPROM.writeString(address, MQTTUser); + address += MQTTUser.length() + 1; + EEPROM.writeString(address, MQTTPwd); + address += MQTTPwd.length() + 1; + EEPROM.writeString(address, MQTTPrefix); + address += MQTTPrefix.length() + 1; + EEPROM.writeString(address, MQTTdeviceName); + address += MQTTdeviceName.length() + 1; + EEPROM.writeString(address, TopicP); + address += TopicP.length() + 1; + EEPROM.writeString(address, TopicT); + address += TopicT.length() + 1; + EEPROM.writeByte(address, subMQTT); + address += sizeof(byte); + EEPROM.writeString(address, nomRouteur); + address += nomRouteur.length() + 1; + EEPROM.writeString(address, nomSondeFixe); + address += nomSondeFixe.length() + 1; + EEPROM.writeString(address, nomSondeMobile); + address += nomSondeMobile.length() + 1; + EEPROM.writeString(address, nomTemperature); + address += nomTemperature.length() + 1; + EEPROM.writeString(address, Source_Temp); + address += Source_Temp.length() + 1; + EEPROM.writeULong(address, IPtemp); + address += sizeof(unsigned long); + EEPROM.writeUShort(address, CalibU); + address += sizeof(unsigned short); + EEPROM.writeUShort(address, CalibI); + address += sizeof(unsigned short); + EEPROM.writeByte(address, TempoEDFon); + address += sizeof(byte); + EEPROM.writeByte(address, WifiSleep); + address += sizeof(byte); + EEPROM.writeByte(address, pSerial); + address += sizeof(byte); + EEPROM.writeByte(address, pTriac); + address += sizeof(byte); + + address += 100; //Réserve de 100 bytes + + //Enregistrement des Actions + EEPROM.writeUShort(address, NbActions); + address += sizeof(unsigned short); + for (int iAct = 0; iAct < NbActions; iAct++) { + EEPROM.writeByte(address, LesActions[iAct].Actif); + address += sizeof(byte); + EEPROM.writeString(address, LesActions[iAct].Titre); + address += LesActions[iAct].Titre.length() + 1; + EEPROM.writeString(address, LesActions[iAct].Host); + address += LesActions[iAct].Host.length() + 1; + EEPROM.writeUShort(address, LesActions[iAct].Port); + address += sizeof(unsigned short); + EEPROM.writeString(address, LesActions[iAct].OrdreOn); + address += LesActions[iAct].OrdreOn.length() + 1; + EEPROM.writeString(address, LesActions[iAct].OrdreOff); + address += LesActions[iAct].OrdreOff.length() + 1; + EEPROM.writeUShort(address, LesActions[iAct].Repet); + address += sizeof(unsigned short); + EEPROM.writeUShort(address, LesActions[iAct].Tempo); + address += sizeof(unsigned short); + EEPROM.writeByte(address, LesActions[iAct].Reactivite); + address += sizeof(byte); + address += 40; //Réserve de 40 bytes + EEPROM.writeByte(address, LesActions[iAct].NbPeriode); + address += sizeof(byte); + for (byte i = 0; i < LesActions[iAct].NbPeriode; i++) { + EEPROM.writeByte(address, LesActions[iAct].Type[i]); + address += sizeof(byte); + EEPROM.writeUShort(address, LesActions[iAct].Hfin[i]); + address += sizeof(unsigned short); + EEPROM.writeShort(address, LesActions[iAct].Vmin[i]); + address += sizeof(unsigned short); + EEPROM.writeShort(address, LesActions[iAct].Vmax[i]); + address += sizeof(unsigned short); + EEPROM.writeShort(address, LesActions[iAct].Tinf[i]); + address += sizeof(unsigned short); + EEPROM.writeShort(address, LesActions[iAct].Tsup[i]); + address += sizeof(unsigned short); + EEPROM.writeByte(address, LesActions[iAct].Tarif[i]); + address += sizeof(byte); + address += 10; //Réserve de 10 bytes + } + } + Calibration(address); + EEPROM.commit(); + return address; +} +void Calibration(int address) { + kV = KV * CalibU / 1000; //Calibration coefficient to be applied + kI = KI * CalibI / 1000; + P_cent_EEPROM = int(100 * address / EEPROM_SIZE); + Serial.println("Mémoire EEPROM utilisée : " + String(P_cent_EEPROM) + "%"); +} + +void init_puissance() { + PuissanceS_T = 0; + PuissanceS_M = 0; + PuissanceI_T = 0; + PuissanceI_M = 0; //Puissance Watt affichée en entiers Maison et Triac + PVAS_T = 0; + PVAS_M = 0; + PVAI_T = 0; + PVAI_M = 0; //Puissance VA affichée en entiers Maison et Triac + PuissanceS_T_inst = 0.0; + PuissanceS_M_inst = 0.0; + PuissanceI_T_inst = 0.0; + PuissanceI_M_inst = 0.0; + PVAS_T_inst = 0.0; + PVAS_M_inst = 0.0; + PVAI_T_inst = 0.0; + PVAI_M_inst = 0.0; + Puissance_T_moy = 0.0; + Puissance_M_moy = 0.0; + PVA_T_moy = 0.0; + PVA_M_moy = 0.0; +} +void filtre_puissance() { //Filtre RC + + float A = 0.3; //Coef pour un lissage en multi-sinus et train de sinus sur les mesures de puissance courte + float B = 0.7; + if (!LissageLong) { + A = 1; + B = 0; + } + + Puissance_T_moy = A * (PuissanceS_T_inst - PuissanceI_T_inst) + B * Puissance_T_moy; + if (Puissance_T_moy < 0) { + PuissanceI_T = -int(Puissance_T_moy); //Puissance Watt affichée en entier Triac + PuissanceS_T = 0; + } else { + PuissanceS_T = int(Puissance_T_moy); + PuissanceI_T = 0; + } + + + Puissance_M_moy = A * (PuissanceS_M_inst - PuissanceI_M_inst) + B * Puissance_M_moy; + if (Puissance_M_moy < 0) { + PuissanceI_M = -int(Puissance_M_moy); //Puissance Watt affichée en entier Maison + PuissanceS_M = 0; + } else { + PuissanceS_M = int(Puissance_M_moy); + PuissanceI_M = 0; + } + + + PVA_T_moy = A * (PVAS_T_inst - PVAI_T_inst) + B * PVA_T_moy; //Puissance VA affichée en entiers + if (PVA_T_moy < 0) { + PVAI_T = -int(PVA_T_moy); + PVAS_T = 0; + } else { + PVAS_T = int(PVA_T_moy); + PVAI_T = 0; + } + + PVA_M_moy = A * (PVAS_M_inst - PVAI_M_inst) + B * PVA_M_moy; + if (PVA_M_moy < 0) { + PVAI_M = -int(PVA_M_moy); + PVAS_M = 0; + } else { + PVAS_M = int(PVA_M_moy); + PVAI_M = 0; + } +} + +void StockMessage(String m) { + m = DATE + " : " + m; + Serial.println(m); + MessageH[idxMessage] = m; + idxMessage = (idxMessage + 1) % 10; +} \ No newline at end of file diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/Temperature.ino b/docs/routers/F1ATB/Solar_Router_V10.00/Temperature.ino new file mode 100644 index 0000000..aa5d9cc --- /dev/null +++ b/docs/routers/F1ATB/Solar_Router_V10.00/Temperature.ino @@ -0,0 +1,91 @@ +// *************** +// * Temperature * +// *************** +void LectureTemperature() { + float temperature_brute = -127; + if (Source_Temp == "tempNo") { + temperature = temperature_brute; + } + if (Source_Temp == "tempInt") { + if (!ds18b20_Init) { + ds18b20_Init = true; + ds18b20.begin(); + } + ds18b20.requestTemperatures(); + temperature_brute = ds18b20.getTempCByIndex(0); + if (temperature_brute < -20 || temperature_brute > 130) { //Invalide. Pas de capteur ou parfois mauvaise réponse + if (TemperatureValide > 0) { + TemperatureValide = TemperatureValide - 1; // Perte éventuels de quelques mesures + } else { + StockMessage("Mesure Température invalide ou pas de capteur DS18B20"); //Trop de pertes + temperature = temperature_brute; + } + } else { + TemperatureValide = 5; + temperature = temperature_brute; + } + } + if (Source_Temp == "tempExt") { + String RMSExtTemp = ""; + + // Use WiFiClient class to create TCP connections + WiFiClient clientESP_RMS; + byte arr[4]; + arr[0] = IPtemp & 0xFF; + arr[1] = (IPtemp >> 8) & 0xFF; + arr[2] = (IPtemp >> 16) & 0xFF; + arr[3] = (IPtemp >> 24) & 0xFF; + + String host = String(arr[3]) + "." + String(arr[2]) + "." + String(arr[1]) + "." + String(arr[0]); + if (!clientESP_RMS.connect(host.c_str(), 80)) { + StockMessage("connection to ESP_RMS Temperature failed : " + host); + delay(200); + ComAbuge(); + if (TemperatureValide > 0) { + TemperatureValide = TemperatureValide - 1; // Perte éventuels de quelques mesures + } else { //Trop de pertes + temperature = temperature_brute; + } + return; + } + String url = "/ajax_Temperature"; + clientESP_RMS.print(String("GET ") + url + " HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Connection: close\r\n\r\n"); + unsigned long timeout = millis(); + while (clientESP_RMS.available() == 0) { + if (millis() - timeout > 5000) { + StockMessage("client ESP_RMS Temperature Timeout !" + host); + clientESP_RMS.stop(); + if (TemperatureValide > 0) { + TemperatureValide = TemperatureValide - 1; // Perte éventuels de quelques mesures + } else { //Trop de pertes + temperature = temperature_brute; + } + return; + } + } + timeout = millis(); + // Lecture des données brutes distantes + while (clientESP_RMS.available() && (millis() - timeout < 5000)) { + RMSExtTemp += clientESP_RMS.readStringUntil('\r'); + } + if (RMSExtTemp.length() > 100) { + RMSExtTemp = ""; + } + if (RMSExtTemp.indexOf(GS) >= 0 && RMSExtTemp.indexOf(RS) > 0) { //Trame complète reçue + RMSExtTemp = RMSExtTemp.substring(RMSExtTemp.indexOf(GS) + 1); + RMSExtTemp = RMSExtTemp.substring(0, RMSExtTemp.indexOf(RS)); + temperature_brute = RMSExtTemp.toFloat(); + RMSExtTemp = ""; + TemperatureValide =5; + temperature = temperature_brute; + } + + } + if (Source_Temp == "tempMqtt") { + if (TemperatureValide > 0) { + TemperatureValide = TemperatureValide - 1; // Watchdog pour verfier mesures arrivent voir MQTT.ino + } else { + temperature = temperature_brute; + } + } +} diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/Tempo_EDF.ino b/docs/routers/F1ATB/Solar_Router_V10.00/Tempo_EDF.ino new file mode 100644 index 0000000..f2be49e --- /dev/null +++ b/docs/routers/F1ATB/Solar_Router_V10.00/Tempo_EDF.ino @@ -0,0 +1,78 @@ +// ******************************************************* +// * Recherche Info Tempo EDF pour les sources non Linky * +// ******************************************************* + + +void Call_EDF_data() { + + const char* adr_EDF_Host = "particulier.edf.fr"; + String Host = String(adr_EDF_Host); + String urlJSON = "/services/rest/referentiel/searchTempoStore?dateRelevant=" + DateEDF; + String EDFdata = ""; + String line = ""; + int Hcour = HeureCouranteDeci / 2; //Par pas de 72secondes pour faire 2 appels si un bug + int LastH = LastHeureEDF / 2; + + if ((LastH != Hcour) && ( Hcour == 300 || Hcour == 310 || Hcour == 530 || Hcour == 560 || Hcour == 600 || Hcour == 900 || Hcour == 1150) || LastHeureEDF < 0) { + if (TempoEDFon == 1) { + // Use clientSecu class to create TCP connections + clientSecuEDF.setInsecure(); //skip verification + if (!clientSecuEDF.connect(adr_EDF_Host, 443)) { + StockMessage("Connection failed to EDF server :" + Host); + } else { + String Request=String("GET ") + urlJSON + " HTTP/1.1\r\n" ; + Request += "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8\r\n" ; + Request += "Accept-Encoding: gzip, deflate, br, zstd\r\n" ; + Request += "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36\r\n" ; + Request += "Host: " + Host + "\r\n" ; + Request += "Connection: keep-alive\r\n\r\n"; + clientSecuEDF.print(Request); + Serial.println("Request vers EDF Envoyé"); + unsigned long timeout = millis(); + while (clientSecuEDF.available() == 0) { + if (millis() - timeout > 5000) { + StockMessage(">>> clientSecuEDF EDF Timeout !"); + clientSecuEDF.stop(); + return; + } + } + timeout = millis(); + // Lecture des données brutes distantes + int fin = 0; + while (clientSecuEDF.connected() && (millis() - timeout < 5000) && fin < 2) { + line = clientSecuEDF.readStringUntil('\n'); + EDFdata += line; + if (line == "\r") { + StockMessage("EnTetes EDF reçues"); + EDFdata = ""; + fin = 1; + } + if (fin == 1 && line.indexOf("}") >= 0) fin = 2; + } + clientSecuEDF.stop(); + + // C'est EDF qui donne la couleur + String LTARFrecu = StringJson("couleurJourJ", EDFdata); //Remplace code du Linky + if (LTARFrecu.indexOf("TEMPO") >= 0) { + LTARF = LTARFrecu; + String couleurJourJ1 = StringJson("couleurJourJ1", EDFdata); + line = "0"; + if (couleurJourJ1 == "TEMPO_BLEU") line = "4"; + if (couleurJourJ1 == "TEMPO_BLANC") line = "8"; + if (couleurJourJ1 == "TEMPO_ROUGE") line = "C"; + STGE = line; //Valeur Hexa code du Linky + StockMessage(DateEDF + " : " + EDFdata); + EDFdata = ""; + LastHeureEDF = HeureCouranteDeci; //Heure lecture Tempo EDF + } else { + StockMessage(DateEDF + " : Pas de données EDF valides"); + } + } + } else { + if (Source != "Linky" && Source != "Ext") { + LTARF = ""; + STGE = "0"; + } + } + } +} diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlActions.h b/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlActions.h new file mode 100644 index 0000000..46b9869 --- /dev/null +++ b/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlActions.h @@ -0,0 +1,665 @@ +//************************************************ +// Page HTML et Javascript de gestion des Actions +//************************************************ + +const char *ActionsHtml = R"====( + + + + + + + +
+ +

Routeur Solaire - RMS

Planning des Routages (suivant sonde Maison)

+
Routage via Triac
+
+
+ + +
+
Routage via Relais
+
+
+
+
+ +
+
+
+

+
Routeur Version :
+ +)===="; +const char *ActionsJS = R"====( + var LesActions = []; + var mouseClick = false; + var blockEvent = false; + var temperatureDS=-127; + var LTARFbin=0; + var pTriac=0; + var IS=String.fromCharCode(31); //Input Separator + function Init() { + LoadActions(); + DispTimer(); + LoadParaRouteur(); + } + function creerAction(aActif, aTitre, aHost, aPort, aOrdreOn, aOrdreOff, aRepet,aTempo,aReactivite, aPeriodes) { + var S = { + Actif: aActif, + Titre: aTitre, + Host: aHost, + Port: aPort, + OrdreOn: aOrdreOn, + OrdreOff: aOrdreOff, + Repet: aRepet, + Tempo: aTempo, + Reactivite: aReactivite, + Periodes: aPeriodes + } + return S; + } + function TracePlanning(iAct) { + var Radio0 = "
Inactif
"; + var Radio1 = "
Découpe sinus
"; + if (iAct > 0){Radio1 = "
On/Off
";} + Radio1 += "
Multi-sinus
"; + Radio1 += "
Train de sinus
"; + var Pins=[0,4,5,14,16,17,21,22,23,25,26,27,-1]; + var SelectPin="Gpio "; + var SelectOut="   Sortie 'On' "; + var S = "

Titre

"; + S +="
" +Radio0 + Radio1 + "
"; + + S +="
"; + S += ""; + S += ""; + S += ""; + S += ""; + S += ""; + S += ""; + S += ""; + S += ""; + S += ""; + S += ""; + S += ""; + S += "
Host
"+SelectPin+SelectOut+"
Ordre On
Répétition(s)Temporisation(s)
Port
Ordre Off
"; + + S +="
"; + S +="
"; + S +="
"; + S +="
Réactivité lente ou charge importante
"; + S +=""; + S +="
Réactivité rapide ou charge faible

"; + S +="
"; + S +="
"; + S += "
"; + S += "
"; + S += "
"; + + GH("planning" + iAct, S); + GID("radio" + iAct +"-" +LesActions[iAct].Actif).checked = true; + GH("titre" + iAct, LesActions[iAct].Titre); + GV("host" + iAct, LesActions[iAct].Host); + GV("port" + iAct, LesActions[iAct].Port); + GV("ordreOn" + iAct, LesActions[iAct].OrdreOn); + GV("ordreOff" + iAct, LesActions[iAct].OrdreOff); + GV("repet" + iAct, LesActions[iAct].Repet); + GV("tempo" + iAct, LesActions[iAct].Tempo); + GV("slider" + iAct ,LesActions[iAct].Reactivite); + GH("sensi" + iAct ,LesActions[iAct].Reactivite); + if(LesActions[iAct].OrdreOn.indexOf(IS)>0){ + var vals=LesActions[iAct].OrdreOn.split(IS); + GID("selectPin"+iAct).value=vals[0]; + GID("SelectOut" + iAct).value=vals[1]; + } else { + GID("selectPin"+iAct).value=-1; + GID("SelectOut" + iAct).value=1; + if(LesActions[iAct].OrdreOn=="") GID("selectPin"+iAct).value=0; + } + TracePeriodes(iAct); + + } + function TracePeriodes(iAct) { + var S = ""; + var Sinfo = ""; + var left = 0; + var H0 = 0; + var colors = ["#666", "#66f", "#f66", "#6f6", "#cc4"]; //NO,OFF,ON,PW,Triac + blockEvent = false; + for (var i = 0; i < LesActions[iAct].Periodes.length; i++) { + var w = (LesActions[iAct].Periodes[i].Hfin - H0) /24; + left = H0 / 24; + H0 = LesActions[iAct].Periodes[i].Hfin; + var Type = LesActions[iAct].Periodes[i].Type; + var color = colors[Type]; + var temperature=""; + if (temperatureDS>-100) { // La sonde de température fonctionne + var Tsup=LesActions[iAct].Periodes[i].Tsup; + if (Tsup>=0 && Tsup <=1000) temperature +="
si T ≥" + Tsup/10 + "°
"; + var Tinf=LesActions[iAct].Periodes[i].Tinf; + if (Tinf>=0 && Tinf <=1000) temperature +="
si T ≤" + Tinf/10 + "°
"; + } + var TxtTarif= ""; + if (LTARFbin>0) { + TxtTarif= " si Tarif : "; + var Tarif_=LesActions[iAct].Periodes[i].Tarif; + if (LTARFbin<=3) { + TxtTarif += (Tarif_ & 1) ? "H. Pleine":"" ; + TxtTarif += (Tarif_ & 2) ? " H. Creuse":"" ; + } else { + TxtTarif += (Tarif_ & 4) ? "TempoBleu":"" ; + TxtTarif += (Tarif_ & 8) ? " Blanc":"" ; + TxtTarif += (Tarif_ & 16) ? " Rouge":"" ; + } + TxtTarif ="
" + TxtTarif +"
"; + } + if (LesActions[iAct].Actif<=1 && iAct>0){ + LesActions[iAct].Periodes[i].Vmax=Math.max(LesActions[iAct].Periodes[i].Vmin,LesActions[iAct].Periodes[i].Vmax); + var TexteMinMax="
Off si Pw>"+LesActions[iAct].Periodes[i].Vmax+"W
On si Pw<"+LesActions[iAct].Periodes[i].Vmin+"W
"+temperature + TxtTarif; + } else { + LesActions[iAct].Periodes[i].Vmax=Math.max(0,LesActions[iAct].Periodes[i].Vmax); + LesActions[iAct].Periodes[i].Vmax=Math.min(100,LesActions[iAct].Periodes[i].Vmax); + var TexteMinMax="
Seuil Pw : "+LesActions[iAct].Periodes[i].Vmin+"W
"+ temperature + "
Ouvre Max : "+LesActions[iAct].Periodes[i].Vmax+"%
" + TxtTarif; + } + var TexteTriac="
Seuil Pw : "+LesActions[iAct].Periodes[i].Vmin+"W
"+temperature + "
Ouvre Max : "+LesActions[iAct].Periodes[i].Vmax+"%
"+TxtTarif; + var paras = ["Pas de contrôle", "OFF", "ON" + temperature + TxtTarif, TexteMinMax, TexteTriac]; + var para = paras[Type]; + S += "
"; + Hmn = Math.floor(H0 / 100) + ":" + ("0" + Math.floor(0.6 * (H0 - 100 * Math.floor(H0 / 100)))).substr(-2, 2); + fs = Math.max(8, Math.min(16, w/2)) + "px"; + Sinfo += "
" + Sinfo += "
" + Hmn + "
" + para + "
"; + } + GH("curseurs" + iAct, S); + GH("infoAction" + iAct, Sinfo); + } + function touchMove(t, ev, iAct) { + var leftPos = ev.touches[0].clientX - GID(t.id).getBoundingClientRect().left; + NewPosition(t, leftPos, iAct); + } + function mouseMove(t, ev, iAct) { + if (mouseClick) { + var leftPos = ev.clientX - GID(t.id).getBoundingClientRect().left; + NewPosition(t, leftPos, iAct); + } + } + function NewPosition(t, leftPos, iAct) { + var G = GID(t.id).style.left; + //+ window.scrollX; + var width = GID(t.id).getBoundingClientRect().width; + var HeureMouse = leftPos * 2420 / width; + var idxClick = 0; + var deltaX = 999999; + for (var i = 0; i < LesActions[iAct].Periodes.length - 1; i++) { + var dist = Math.abs(HeureMouse - LesActions[iAct].Periodes[i].Hfin) + if (dist < deltaX) { + idxClick = i; + deltaX = dist; + } + } + var NewHfin = Math.max(0, Math.min(HeureMouse, 2400)); + if (idxClick == LesActions[iAct].Periodes.length - 1) NewHfin=2400; + if (idxClick < LesActions[iAct].Periodes.length - 1) + NewHfin = Math.min(NewHfin, LesActions[iAct].Periodes[idxClick + 1].Hfin); + if (idxClick > 0) + NewHfin = Math.max(NewHfin, LesActions[iAct].Periodes[idxClick - 1].Hfin); + LesActions[iAct].Periodes[idxClick].Hfin = Math.floor(NewHfin); + TracePeriodes(iAct); + + } + function AddSub(v, iAct) { + if (v == 1) { + if (LesActions[iAct].Periodes.length<8){ + LesActions[iAct].Periodes.push({ + Hfin: 2400, + Type: 1, + Vmin:0, + Vmax:100, + Tinf:1500, + Tsup:1500, + Tarif:31 + }); //Tarif codé en bits + var Hbas = 0; + if (LesActions[iAct].Periodes.length > 2){ + Hbas = parseInt(LesActions[iAct].Periodes[LesActions[iAct].Periodes.length - 3].Hfin); + } + if (LesActions[iAct].Periodes.length > 1) { + LesActions[iAct].Periodes[LesActions[iAct].Periodes.length - 2].Hfin = Math.floor((Hbas + 2400) / 2); + } + } + } else { + if (LesActions[iAct].Periodes.length>1){ + LesActions[iAct].Periodes.pop(); + if (LesActions[iAct].Periodes.length > 0) + LesActions[iAct].Periodes[LesActions[iAct].Periodes.length - 1].Hfin = 2400; + } + } + TracePeriodes(iAct); + + } + function infoZclicK(i, iAct) { + if (!blockEvent) { + blockEvent = true; + var Type = LesActions[iAct].Periodes[i].Type; + var idZ = "info" + iAct + "Z" + i; + var S = "
Sélection Action
X
"; + //On ne traite plus depuis version8 le cas "Pas de Contrôle". Inutile + c = (Type == 1) ? "bInset" : "bOutset"; + S += "
OFF
"; + S += ""; + S += "
"; + if (temperatureDS>-100) { + S += "
"; + S += "
Actif si température :
"; + S += "
T ≥°
"; + S += "
T ≤°
"; + S += "
T en degré (0.0 à 100.0) ou laisser vide
"; + S += "
"; + } + if (LTARFbin>0) { + + S += "
"; + S += "
Actif si tarif :
"; + if (LTARFbin<=3) { + S += "
Heure Pleine Heure Creuse
"; + } else { + S += "
Tempo Bleu Blanc Rouge
"; + } + + S += "
"; + } + S += "
"; + S += ""; + GH(idZ, S); + var Tarif_=LesActions[iAct].Periodes[i].Tarif; + if (LTARFbin>0) { + if (LTARFbin<=3) { + GID("TarifPl_" + idZ).checked = (Tarif_ & 1) ? 1:0 ; // H Pleine + GID("TarifCr_" + idZ).checked = (Tarif_ & 2) ? 1:0 ; + } else { + GID("TarifBe_" + idZ).checked = (Tarif_ & 4) ? 1:0 ; + GID("TarifBa_" + idZ).checked = (Tarif_ & 8) ? 1:0 ; + GID("TarifRo_" + idZ).checked = (Tarif_ & 16) ? 1:0 ; //Rouge + } + } + GID(idZ).style.display = "block"; + } + } + function infoZclose(idx) { + var champs=idx.split("info"); + var idx=champs[1].split("Z"); + S="TracePeriodes("+idx[0]+");" + setTimeout(S, 100); + } + function selectZ(T, i, iAct) { + if (LesActions[iAct].Periodes[i].Type != T) { + LesActions[iAct].Periodes[i].Type = T; + var idZ = "info" + iAct + "Z" + i; + if (T <= 2) + infoZclose(idZ); + TracePeriodes(iAct); + } + } +)===="; +const char *ActionsJS2 = R"====( + function NewVal(t){ + var champs=t.id.split("info"); + var idx=champs[1].split("Z"); //Num Action, Num période + if (champs[0].indexOf("min")>0){ + LesActions[idx[0]].Periodes[idx[1]].Vmin=Math.floor(GID(t.id).value); + } + if (champs[0].indexOf("max")>0){ + LesActions[idx[0]].Periodes[idx[1]].Vmax=Math.floor(GID(t.id).value); + if (idx[0]==0){ + LesActions[idx[0]].Periodes[idx[1]].Vmax=Math.max(LesActions[idx[0]].Periodes[idx[1]].Vmax,5); + LesActions[idx[0]].Periodes[idx[1]].Vmax=Math.min(LesActions[idx[0]].Periodes[idx[1]].Vmax,100); + } + } + if (champs[0].indexOf("inf")>0){ + var V= GID(t.id).value; + if (V=="") V=128; + LesActions[idx[0]].Periodes[idx[1]].Tinf=Math.floor(V*10); + } + if (champs[0].indexOf("sup")>0){ + var V= GID(t.id).value; + if (V=="") V=128; + LesActions[idx[0]].Periodes[idx[1]].Tsup=Math.floor(V*10); + } + + if (champs[0].indexOf("Tarif")>=0){ + var idZ = "info" + champs[1]; + var Tarif_ = 0; + if (LTARFbin<=3) { + Tarif_ += GID("TarifPl_" + idZ).checked ? 1:0; //H pleine + Tarif_ += GID("TarifCr_" + idZ).checked ? 2:0; + } else { + Tarif_ += GID("TarifBe_" + idZ).checked ? 4:0; //Bleu + Tarif_ += GID("TarifBa_" + idZ).checked ? 8:0; + Tarif_ += GID("TarifRo_" + idZ).checked ? 16:0; //Rouge + } + LesActions[idx[0]].Periodes[idx[1]].Tarif=Tarif_; + } + } + function editTitre(iAct) { + if (GID("titre" + iAct).innerHTML.indexOf(""); + GID("Etitre" + iAct).focus(); + } + } + function TitreValid(iAct) { + LesActions[iAct].Titre = GID("Etitre" + iAct).value.trim(); + GH("titre" + iAct, LesActions[iAct].Titre); + } + function checkDisabled(){ + for (var iAct = 0; iAct < LesActions.length; iAct++) { + for (var i=0;i<=3;i++){ + if( GID("radio" + iAct +"-"+ i).checked ) { LesActions[iAct].Actif =i;} + } + TracePeriodes(iAct); + GID("planning0").style.display = (pTriac>0) ? "block" : "none"; // Si Pas de Triac + GID("TitrTriac").style.display = (pTriac>0) ? "block" : "none"; + GID("blocPlanning"+iAct).style.display = (LesActions[iAct].Actif>0) ? "block" : "none"; + var visible = ( LesActions[iAct].Actif== 1) ? "visible" : "hidden"; + GID("Tempo"+iAct).style.visibility =visible; + GID("tempo"+iAct).style.visibility =visible; + var disable=true; + var disp="block"; + if (GID("selectPin"+iAct).value>=0) { visible="hidden";disable=false;disp="none";} + GID("SelectOut"+iAct).style.display = (GID("selectPin"+iAct).value<=0) ? "none":"inline-block"; + GID("Host"+iAct).style.visibility =visible; + GID("host"+iAct).style.visibility =visible; + GID("Port"+iAct).style.visibility =visible; + GID("port"+iAct).style.visibility =visible; + GID("Repet"+iAct).style.visibility =visible; + GID("repet"+iAct).style.visibility =visible; + GID("radio" + iAct +"-2").disabled = disable; + GID("radio" + iAct +"-3").disabled = disable; + GID("ordreoff"+iAct).style.display=disp; + GID("ordreon"+iAct).style.display =disp; + if (GID("selectPin"+iAct).value==-1 && GID("ordreOn"+iAct).value.indexOf(IS)>0) GID("ordreOn"+iAct).value=""; + GID("ligne_bas"+iAct).style.display =( LesActions[iAct].Actif> 1) ? "none" :"table-row"; + GID("fen_slide"+iAct).style.visibility = (LesActions[iAct].Actif== 1 && iAct>0 ) ? "hidden" : "visible"; + } + } + function LoadActions() { + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function () { + if (this.readyState == 4 && this.status == 200) { + var LeRetour = this.responseText; + var Les_ACTIONS = LeRetour.split(GS); + var LesParas = Les_ACTIONS[0].split(RS); + temperatureDS=LesParas[0]; + LTARFbin = parseInt(LesParas[1]); + pTriac = parseInt(LesParas[2]); + LesActions.splice(0,LesActions.length); + for (var iAct=1;iAct"; + } + GH("plannings", S); + for (var iAct = 0; iAct < LesActions.length; iAct++) { + TracePlanning(iAct); + } + checkDisabled(); + + } + }; + xhttp.open('GET', 'ActionsAjax', true); + xhttp.send(); + } + + + function SendValues() { + GID("attente").style="visibility: visible;"; + for (var iAct = 0; iAct < LesActions.length; iAct++) { + for (var i=0;i<=3;i++){ + if( GID("radio" + iAct +"-"+ i).checked ) { LesActions[iAct].Actif =i;} + } + LesActions[iAct].Titre = GID("titre" + iAct).innerHTML.trim(); + LesActions[iAct].Host = GID("host" + iAct).value.trim(); + LesActions[iAct].Port = GID("port" + iAct).value; + LesActions[iAct].OrdreOn = GID("ordreOn" + iAct).value.trim(); + LesActions[iAct].OrdreOff = GID("ordreOff" + iAct).value.trim(); + LesActions[iAct].Repet = GID("repet" + iAct).value; + LesActions[iAct].Tempo = GID("tempo" + iAct).value; + LesActions[iAct].Reactivite = GID("slider" + iAct).value; + if (GID("selectPin"+iAct).value>=0) LesActions[iAct].OrdreOn=GID("selectPin"+iAct).value +IS + GID("selectOut"+iAct).value; + if (GID("selectPin"+iAct).value==0 && iAct>0) LesActions[iAct].Actif=-1; //Action à effacer + } + var S=""; + for (var iAct = 0; iAct < LesActions.length; iAct++) { + if (LesActions[iAct].Actif>=0){ + S +=LesActions[iAct].Actif+RS+LesActions[iAct].Titre+RS; + S +=LesActions[iAct].Host+RS+LesActions[iAct].Port+RS; + S +=LesActions[iAct].OrdreOn+RS+LesActions[iAct].OrdreOff+RS+LesActions[iAct].Repet+RS+LesActions[iAct].Tempo+RS; + S +=LesActions[iAct].Reactivite + RS + LesActions[iAct].Periodes.length+RS; + for (var i=0;i + + +
+ +

Routeur Solaire - RMS

+
Date


+
+
Tension et Courant sur 20ms
+

_ V + _ A
+ Facteur de puissance :

+
+
+

Données brutes capteur JSY-MK-194T
+
+
+
+
Données brutes capteur JSY-MK-333
+
+
+
+

Données Enphase Envoy-S Metered
+
+
+
+
Données SmartGateways
+
+
+
+
Données Shelly Em
+
+
+
+
Données puissances recues par MQTT
+
+
+
+
+
+

Données brutes Linky en mode standard
+
+
+
Données distantes
+
Données ESP32
+

+
Routeur Version :
+ + +
+)===="; + +const char *PageBruteJS = R"====( + var InitFait=false; + var IdxMessage=0; + var MessageLinky=''; + + const M=[]; //Pour UxIx2 + M.push(['Tension_M','Tension efficace','V','V']); + M.push(['Intensite_M','Courant efficace','A','A']); + M.push(['PuissanceS_M','Puissance (Pw)','W','W']); + M.push(['PowerFactor_M','Facteur de puissance','','phi']); + M.push(['Energie_M_Soutiree','Energie active soutirée','Wh','Wh']); + M.push(['Energie_M_Injectee','Energie active injectée','Wh','Wh']); + M.push(['Tension_T','Tension efficace','V','V']); + M.push(['Intensite_T','Courant efficace','A','A']); + M.push(['PuissanceS_T','Puissance (Pw)','W','W']); + M.push(['PowerFactor_T','Facteur de puissance','','phi']); + M.push(['Energie_T_Soutiree','Energie active consommée','Wh','Wh']); + M.push(['Energie_T_Injectee','Energie active produite','Wh','Wh']); + M.push(['Frequence','Fréquence','Hz','Hz']); + const E=[]; //Pour Enphase + E.push(['Tension_M','Tension efficace','V','V']); + E.push(['Intensite_M','Courant efficace','A','A']); + E.push(['PuissanceS_M','Puissance réseau public (Pw)','W','W']); + E.push(['PowerFactor_M','Facteur de puissance','','phi']); + E.push(['Energie_M_Soutiree','Energie active soutirée','Wh','Wh']); + E.push(['Energie_M_Injectee','Energie active injectée','Wh','Wh']); + E.push(['PactProd','Puissance produite (Pw)','W','W']); + E.push(['PactConso_M','Puissance consommée (Pw)','W','W']); + E.push(['SessionId','Session Id','','Enph']); + E.push(['Token_Enphase','Token','','Enph']); + + const L=[]; + L.push(['EAST','Energie active soutirée',false,'Wh',0]); + L.push(['EASF01','Energie active soutirée Fournisseur,
index 01',true,'Wh',0]); + L.push(['EASF02','Energie active soutirée Fournisseur,
index 02',true,'Wh',0]); + L.push(['EASF03','Energie active soutirée Fournisseur,
index 03',true,'Wh',0]); + L.push(['EASF04','Energie active soutirée Fournisseur,
index 04',true,'Wh',0]); + L.push(['EASF05','Energie active soutirée Fournisseur,
index 05',true,'Wh',0]); + L.push(['EASF06','Energie active soutirée Fournisseur,
index 06',true,'Wh',0]); + L.push(['EASF07','Energie active soutirée Fournisseur,
index 07',true,'Wh',0]); + L.push(['EASF08','Energie active soutirée Fournisseur,
index 08',true,'Wh',0]); + L.push(['EASF09','Energie active soutirée Fournisseur,
index 09',true,'Wh',0]); + L.push(['EASF10','Energie active soutirée Fournisseur,
index 10',true,'Wh',0]); + L.push(['EAIT','Energie active injectée',false,'Wh',0]); + L.push(['IRMS1','Courant efficace, phase 1',true,'A',0]); + L.push(['IRMS2','Courant efficace, phase 2',true,'A',0]); + L.push(['IRMS3','Courant efficace, phase 3',true,'A',0]); + L.push(['URMS1','Tension efficace, phase 1',true,'V',0]); + L.push(['URMS2','Tension efficace, phase 2',true,'V',0]); + L.push(['URMS3','Tension efficace, phase 3',true,'V',0]); + L.push(['SINSTS','Puissance app. Instantanée soutirée',false,'VA',0]); + L.push(['SINSTS1','Puissance app. Instantanée soutirée phase 1',true,'VA',0]); + L.push(['SINSTS2','Puissance app. Instantanée soutirée phase 2',true,'VA',0]); + L.push(['SINSTS3','Puissance app. Instantanée soutirée phase 3',true,'VA',0]); + L.push(['SMAXSN','Puissance app. max. soutirée n',false,'VA',1]); + L.push(['SMAXSN1','Puissance app. max. soutirée n phase 1',true,'VA',1]); + L.push(['SMAXSN2','Puissance app. max. soutirée n phase 2',true,'VA',1]); + L.push(['SMAXSN3','Puissance app. max. soutirée n phase 3',true,'VA',1]); + L.push(['SMAXSN-1','Puissance app. max. soutirée n-1',false,'VA',1]); + L.push(['SMAXSN1-1','Puissance app. max. soutirée n-1 phase 1',true,'VA',1]); + L.push(['SMAXSN2-1','Puissance app. max. soutirée n-1 phase 2',true,'VA',1]); + L.push(['SMAXSN3-1','Puissance app. max. soutirée n-1 phase 3',true,'VA',1]); + L.push(['SINSTI','Puissance app. Instantanée injectée',false,'VA',0]); + L.push(['SMAXIN','Puissance app. max injectée n',false,'VA',1]); + L.push(['SMAXIN-1','Puissance app. max injectée n-1',false,'VA',1]); + L.push(['LTARF','Option Tarifaire',false,'',2]); + + function creerTableauUxIx2(){ + var S=''; + for (var i=0;i'; + } + S+='
'+M[i][1]+''+M[i][2]+'
'; + GH('tableau', S); + } + function creerTableauEnphase(){ + var S=''; + for (var i=0;i'; + } + S+='
'+E[i][1]+''+E[i][2]+'
'; + GH('tableauEnphase', S); + } + function creerTableauLinky(){ + var S=''; + for (var i=0;i'; + } + S+='
'+L[i][1]+''+L[i][3]+'
'; + GH('tableauLinky', S); + } + function LoadData() { + GID('LED').style='display:block;'; + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (this.readyState == 4 && this.status == 200) { + GID('LED').style='display:none;'; + var DuRMS=this.responseText; + var groupes=DuRMS.split(GS) + var G0=groupes[0].split(RS); + GH('date',G0[0]); + Source_data=G0[1]; + if (Source_data == "UxI"){ + GID('infoUxI').style.display="block"; + GH('Ueff',parseInt(G0[2],10)); + GH('Ieff',G0[3]); + GH('cosphi',G0[4]); + var volt=groupes[1].split(RS); + var amp=groupes[2].split(RS); + var S= ""; + S += ""; + S += ""; + var Vmax = 500; + var Imax = 500; + for (var i = 0; i < 100; i++) { + Vmax = Math.max(Math.abs(volt[i]), Vmax); + Imax = Math.max(Math.abs(amp[i]), Imax); + } + + S += ""; + } + GH('dataSmartG', S); + } + if (Source_data == "UxIx3"){ + GID('infoUxIx3').style.display="block"; + GH('dataUxIx3', groupes[1]); + } + if (Source_data == "ShellyEm"){ + GID('infoShellyEm').style.display="block"; + groupes[1] = groupes[1].replaceAll('"',''); + var G1=groupes[1].split(","); + var S=""; + for (var i=0;i"; + } + GH('dataShellyEm', S); + } + if (Source_data == "Pmqtt"){ + GID('infoPmqtt').style.display="block"; + GH('dataPmqtt', groupes[1]); + } + if (Source_data == "Linky"){ + GID('infoLinky').style.display="block"; + if(!InitFait){ + InitFait=true; + creerTableauLinky(); + } + MessageLinky +=groupes[1]; + var blocs=MessageLinky.split(String.fromCharCode(2)); + var lg=blocs.length; + if (lg>2){ + MessageLinky=String.fromCharCode(2)+blocs[lg-1]; + GH('DataLinky', '
'+blocs[lg-2]+'
'); + var lignes=blocs[lg-2].split(String.fromCharCode(10)); + for (var i=0;i0){ + GID('L'+L[j][0]).style.display="table-row"; + switch (L[j][4]){ + case 0: + GH(L[j][0], LaVal(colonnes[1])); + break; + case 1: + GH('h'+L[j][0], LaDate(colonnes[1])); + GH(L[j][0], LaVal(colonnes[2])); + break; + case 2: //Texte + GH('h'+L[j][0], colonnes[1]); + break; + } + } + } + } + } + GID('LED').style='display:none;'; + } + IdxMessage=groupes[2]; + } + + setTimeout('LoadData();',2000); + } + }; + xhttp.open('GET', 'ajax_dataRMS?idx='+IdxMessage, true); + xhttp.send(); + } + function LoadDataESP32() { + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (this.readyState == 4 && this.status == 200) { + var dataESP=this.responseText; + var message=dataESP.split(RS); + var S=''; + var H=parseInt(message[0]); + H=H + (message[0]-H)*0.6; + H=H.toFixed(2); + H=H.replace(".", "h ")+"mn"; + var LaSource=Source; + if (LaSource=='Ext') LaSource="Externe ("+Source_data+")
" +int2ip(RMSextIP); + S+=''; + S+=''; + S+=''; + S+="'; + S+=''; + S+=''; + S+=''; + S+=''; + S+=''; + S+=''; + S+=''; + S+=''; + S+="'; + S+=''; + S +=''; + for (var i=0;i<10;i++){ + S +=''; + } + S+='
ESP On depuis :'+H+'
Source des mesures :'+LaSource+'
Niveau WiFi :'+message[1]+' dBm
Point d'accès WiFi :"+message[2]+'
Adresse MAC ESP32 :'+message[3]+'
Réseau WiFi :'+message[4]+'
Adresse IP ESP32 :'+message[5]+'
Adresse passerelle :'+message[6]+'
Masque du réseau :'+message[7]+'
Charge coeur 0 (Lecture Puissance) Min, Moy, Max :'+message[8]+' ms
Charge coeur 1 (Calcul + Wifi) Min, Moy, Max :'+message[9]+' ms
Espace mémoire EEPROM utilisé :'+message[10]+' %
Nombre d'interruptions en 15ms du Gradateur (signal Zc) : Filtrés/Brutes :"+message[11]+'
Synchronisation 10ms au Secteur ou asynchrone horloge ESP32'+message[12]+'
Messages
'+message[13+i]+'
'; + GH('DataESP32', S); + setTimeout('LoadDataESP32();',5000); + } + + }; + xhttp.open('GET', 'ajax_dataESP32', true); + xhttp.send(); + } + function LaDate(d){ + return d.substr(0,1)+' '+d.substr(5,2)+'/'+d.substr(3,2)+'/'+d.substr(1,2)+' '+d.substr(7,2)+'h '+d.substr(9,2)+'mn '+d.substr(11,2)+'s'; + } + function LaVal(d){ + d=parseInt(d); + d=' '+d.toString(); + return d.substr(-9,3)+' '+d.substr(-6,3)+' '+d.substr(-3,3); + } + function AdaptationSource(){ + if(Source=="Ext"){ + GID("donneeDistante").style.display="block"; + } + LoadData();LoadDataESP32(); + } +)===="; + diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlConnect.h b/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlConnect.h new file mode 100644 index 0000000..c4e0235 --- /dev/null +++ b/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlConnect.h @@ -0,0 +1,106 @@ +// ********************************************************** +// Page de connexion 'Acces Point' pour définir réseau WIFI +// ********************************************************** +const char *ConnectAP_Html = R"====( + + + + + + + +

Routeur Solaire - RMS

Connexion au réseau WIFI local

+


+
+ +
+
+
+
Entrez le mot de passe du réseau :
+ +
+
+ +
+
+
Attendez l'adresse IP attribuée à l'ESP 32
+
+ + +)===="; diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlMain.h b/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlMain.h new file mode 100644 index 0000000..5e21a1d --- /dev/null +++ b/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlMain.h @@ -0,0 +1,553 @@ +//************************************************ +// Page principale HTML et Javascript +//************************************************ +const char *MainHtml = R"====( + + + +
+ +

Routeur Solaire - RMS

+
DATE
+
+ + + + + + +
MaisonFixe
SoutiréeInjectéeConso.Produite
Puissance Active (Pw)W
Puissance ApparenteVA
Energie Active du jourWh
Energie Active TotaleWh
+
Données distantes
+
+

+

+

+

+

+

+

+
+

Données RMS
+
Routeur Version :
+ + +
+)===="; + +const char *MainJS = R"====( + var tabPW2sM=[]; + var tabPW2sT=[]; + var initUxIx2=false; + var biSonde=false; + var TabVal = []; + var TabCoul= []; + var myTimeout; + var myActionTimeout; + var ActionForce =[]; + var Pva_valide =false; + function LoadData() { + GID('LED').style='display:block;'; + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (this.readyState == 4 && this.status == 200) { + var DuRMS=this.responseText; + var groupes=DuRMS.split(GS); + var G0=groupes[0].split(RS); + var G1=groupes[1].split(RS); + var G2=groupes[2].split(RS); + GID('date').innerHTML = G0[1]; + Source_data= G0[2]; + if (!initUxIx2){ + initUxIx2=true; + var d='none'; + if(groupes.length==4){ // Cas pour les sources externes UxIx2 et Shelly monophasé + d="table-cell"; + } + const collection = document.getElementsByClassName('dispT'); + for (let i = 0; i < collection.length; i++) { + collection[i].style.display = d; + } + } + GID('PwS_M').innerHTML = LaVal(G1[0]); //Maison + GID('PwI_M').innerHTML = LaVal(G1[1]); //Maison + GID('PVAS_M').innerHTML = LaVal(G1[2]); //Maison + GID('PVAI_M').innerHTML = LaVal(G1[3]); //Maison + GID('EAJS_M').innerHTML = LaVal(G1[4]); + GID('EAJI_M').innerHTML = LaVal(G1[5]); + GID('EAS_M').innerHTML = LaVal(G1[6]); + GID('EAI_M').innerHTML = LaVal(G1[7]); + tabPW2sM.shift(); //Enleve Pw Maison + tabPW2sM.shift(); //Enleve PVA + tabPW2sM.push(parseFloat(G1[0]-G1[1])); + tabPW2sM.push(parseFloat(G1[2]-G1[3])); + Plot('SVG_PW2sM',tabPW2sM,'#f44','Puissance Active '+GID("nomSondeMobile").innerHTML+' sur 10 mn en W','aqua','Puissance Apparente sur 10 mn en VA'); + + var Tarif=["NON_DEFINI","PLEINE","CREUSE","BLEU","BLANC","ROUGE"]; + var couleur=["#ddf","#f00","#0f0","#00bfff","#fff","#f00"]; + var tarif=["","H.
pleine","H.
creuse","Tempo
Bleu","Tempo
Blanc","Tempo
Rouge"]; + var idx=0; + for (i=0;i<6;i++){ + if ( G0[3].indexOf(Tarif[i])>=0){ //LTARF dans Link + idx=i; + } + } + GID('couleurTarif_jour').style.backgroundColor= couleur[idx]; + GID('couleurTarif_jour').innerHTML =tarif[idx]; + var tempo = parseInt(G0[4], 16); //Tempo lendemain et jour STGE + tempo =Math.floor(tempo/4) ; //Tempo lendemain uniquement + idx=-2; + var txtJ = ""; + if (tempo>0){ + idx = tempo; + txtJ = "Tempo
J+1"; + } + GID('couleurTarif_J1').style.backgroundColor= couleur[idx+2]; + GID('couleurTarif_J1').innerHTML =txtJ; + Pva_valide = (G0[6] == 1 ) ? true:false; + + if (groupes.length==4) { // La source_data des données est de type UxIx2 ou on est en shelly monophas avec un deuxièeme canal + GID('PwS_T').innerHTML = LaVal(G2[0]); //Triac + GID('PwI_T').innerHTML = LaVal(G2[1]); //Triac + GID('PVAS_T').innerHTML = LaVal(G2[2]); //Triac + GID('PVAI_T').innerHTML = LaVal(G2[3]); //Triac + GID('EAJS_T').innerHTML = LaVal(G2[4]); + GID('EAJI_T').innerHTML = LaVal(G2[5]); + GID('EAS_T').innerHTML = LaVal(G2[6]); + GID('EAI_T').innerHTML = LaVal(G2[7]); + tabPW2sT.shift(); //Enleve Pw Triav + tabPW2sT.shift(); //Enleve PVA + tabPW2sT.push(parseFloat(G2[0]-G2[1])); + tabPW2sT.push(parseFloat(G2[2]-G2[3])); + Plot('SVG_PW2sT',tabPW2sT,'#f44','Puissance Active '+GID("nomSondeFixe").innerHTML+' sur 10 mn en W','aqua','Puissance Apparente sur 10 mn en VA'); + if (parseInt(G2[5])==0 && Source!="ShellyEm") { //Il n'y a pas d'injecté normalement + GID('produite').innerHTML=''; + GID('PwI_T').innerHTML=''; + GID('PVAI_T').innerHTML=''; + GID('EAJI_T').innerHTML=''; + GID('EAI_T').innerHTML=''; + } + biSonde=true; + } else{ + biSonde=false; + } + if (!Pva_valide) { GID('ligneVA').style='display:none;';} + GID('LED').style='display:none;'; + setTimeout('LoadData();',2000); + } + + }; + xhttp.open('GET', 'ajax_data', true); + xhttp.send(); + } + + function LoadHisto10mn() { + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (this.readyState == 4 && this.status == 200) { + var retour=this.responseText; + var groupes=retour.split(GS); + tabPW2sM.splice(0,tabPW2sM.length); + tabPW2sM=groupes[1].split(','); + tabPW2sM.pop(); + Plot('SVG_PW2sM',tabPW2sM,'#f44','Puissance Active '+GID("nomSondeMobile").innerHTML+' sur 10 mn en W','aqua','Puissance Apparente sur 10 mn en VA'); + if (biSonde){ + tabPW2sT.splice(0,tabPW2sT.length); + tabPW2sT=groupes[2].split(','); + tabPW2sT.pop(); + GID('SVG_PW2sT').style.display="block"; + Plot('SVG_PW2sT',tabPW2sT,'#f44','Puissance Active '+GID("nomSondeFixe").innerHTML+' sur 10 mn en W','aqua','Puissance Apparente sur 10 mn en VA'); + } + LoadHisto1an(); + } + + }; + xhttp.open('GET', 'ajax_data10mn', true); + xhttp.send(); + } + function LoadHisto48h() { + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (this.readyState == 4 && this.status == 200) { + var retour=this.responseText; + var groupes=retour.split(GS); + var tabPWM=groupes[1].split(','); + tabPWM.pop(); + Plot('SVG_PW48hM',tabPWM,'#f33','Puissance Active '+GID("nomSondeMobile").innerHTML+' sur 48h en W','',''); + if (biSonde){ + var tabPWT=groupes[2].split(','); + tabPWT.pop(); + GID('SVG_PW48hT').style.display="block"; + Plot('SVG_PW48hT',tabPWT,'#f33','Puissance Active '+GID("nomSondeFixe").innerHTML+' sur 48h en W','',''); + } + groupes.shift();groupes.shift();groupes.shift(); + if (parseFloat(groupes[0])> -100) { + var tabTemperature=groupes[1].split(','); + tabTemperature.pop(); + GID('SVG_Temp48h').style.display="block"; + Plot('SVG_Temp48h',tabTemperature,'#3f3',nomTemperature+' sur 48h ','',''); + } + groupes.shift(); + groupes.shift(); + if (groupes.length>0) { + Plot_ouvertures(groupes); + } + setTimeout('LoadHisto48h();',300000); + } + + }; + xhttp.open('GET', 'ajax_histo48h', true); + xhttp.send(); + } + function LoadHisto1an() { + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (this.readyState == 4 && this.status == 200) { + var retour=this.responseText; + var tabWh=retour.split(','); + tabWh.pop(); + + Plot('SVG_Wh1an',tabWh,'#ff4','Energie Active Wh / Jour sur 1an','',''); + LoadHisto48h(); + } + + }; + xhttp.open('GET', 'ajax_histo1an', true); + xhttp.send(); + } + function Plot(SVG,Tab,couleur1,titre1,couleur2,titre2){ + var Vmax=0; + var Vmin=0; + var TabY0=[]; + var TabY1=[]; + for (var i = 0; i < Tab.length; i++) { + Tab[i]=Math.min(Tab[i],10000000); + Tab[i]=Math.max(Tab[i],-10000000); + Vmax = Math.max(Math.abs(Tab[i]), Vmax); + } + var cadrageMax=1; + var cadrage1=1000000; + var cadrage2=[10,8,5,4,2,1]; + for (var m=0;m<7;m++){ + for (var i=0;i"; // + S += ""; + S += ""; + + for (var x=1000+X0;x>100;x=x-pixelTic){ + var X=x; + var Y2=Y0+6; + S +=""; + X=X-8; + Y2=Y0+22; + if (SVG=='SVG_Wh1an') { + X=X+8; + S +=""+Mois[H00]+""; + }else{ + S +=""+H00+""; + } + H00=(H00-dTextTic+moduloText)%moduloText; + } + Y2=Y0-3; + S +=""+label+""; + for (var y=-10 ;y<=10;y=y+dy){ + + Y2=Y0-Yamp*y/10; + if (Y2<=480){ + S +=""; + Y2=Y2+7; + var T=cadrageMax*y/10;T=T.toString(); + var X=90-9*T.length; + S +=""+T+""; + } + } + if (dI==2 && Pva_valide){ //Puissance apparente + S +=""+titre2+""; + S += ""; + } + S +=""+titre1+""; + S += ""; + S += ""; + GID(SVG).innerHTML = S; + TabVal["S_" + SVG]=[TabY0,TabY1];; //Sauvegarde valeurs + TabCoul["S_" + SVG]=[couleur1, couleur2]; + + } + + function DispVal(t,evt){ + var ClientRect = t.getBoundingClientRect(); + var largeur_svg=ClientRect.right-ClientRect.left-20; //20 pixels de marge + var x= Math.round(evt.clientX - ClientRect.left-10); + x=x*1030/largeur_svg; + var p=Math.floor((x-100)*TabVal[t.id][0].length/900); + if (p>=0 && p0) { + S +="
" + TabVal[t.id][j][p] + "
"; + } + } + x = evt.pageX; + GID("info").style.left=x + "px"; + x = ClientRect.top+10 +window.scrollY; + GID("info").style.top=x +"px"; + x=evt.pageY- x; + GID("info_txt").style.top=x +"px"; + x = ClientRect.height-20; + GID("info").style.height=x +"px"; + GH("info_txt",S); + GID("info").style.display="block"; + if (myTimeout !=null) clearTimeout(myTimeout); + myTimeout=setTimeout(stopAffiche, 5000); + } + + } + function stopAffiche(){ + GID("info").style.display="none"; + } + function Plot_ouvertures(Gr){ + GID("SVG_Ouvertures").style.display="block"; + const d = new Date(); + var label='heure'; + var pixelTic=72; + var dTextTic=4; + var moduloText=24; + var H0=d.getHours()+d.getMinutes()/60; + var H00= 4*Math.floor(H0/4); + var X0=18*(H00-H0); + var Hmax=50+150*Gr.length; + var Y0=Hmax-50; + var Couls=["#f83","#3fa","#68f"]; + var LesVals =[]; + var LesCouls =[]; + var S= ""; + S += ""; + for (var x=1000+X0;x>100;x=x-pixelTic){ + var X=x; + var Y2=Y0+6; + S +=""; + X=X-8; + Y2=Y0+22; + S +=""+H00+""; + H00=(H00-dTextTic+moduloText)%moduloText; + } + for (var i=0;i"+ tableau.pop() +""; + S += ""; + Y2= Y00 - 100; + S += ""; + Y2= Y00 +7; + S +="0"; + Y2= Y00 -93; + S +="100"; + Y2 = Y00-100; + S += ""; + S += ""; + + LesVals.push(tableau); //Sauvegarde valeurs + LesCouls.push(Couls[i%3]); + } + S += ""; + TabVal["S_Ouvertures"] = LesVals; + TabCoul["S_Ouvertures"] = LesCouls; + GID("SVG_Ouvertures").innerHTML = S; + + } + function EtatActions(Force,NumAction) { + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (this.readyState == 4 && this.status == 200) { + var retour=this.responseText; + var message=retour.split(GS); + + Source_data=message[1]; + var T=""; + if(message[0]>-100){ + var Temper=parseFloat(message[0]).toFixed(1); + T="" + nomTemperature +""+Temper+"°C"; + } + var S=""; + if (message[3]>0){ //Nb Actions + ActionForce.splice(0, ActionForce.length); + for (var i=0;i"; + if (data[2]=="On" || data[2]=="Off"){ + S+="
"+data[2]+"
"; + } else { + var W=1+1.99*data[2]; + S+="
"+data[2]+"%
"; + } + var stOn=(ActionForce[i]>0) ? "style='background-color:#f66;'":""; + var stOff=(ActionForce[i]<0) ? "style='background-color:#f66;'":""; + var min=(ActionForce[i]==0) ? "  ":Math.abs(ActionForce[i]) +" min"; + S +=""+min+""; + } + } + S=S+T; + if (S!=""){ + S="
" +S + "
Etat Action(s) Forçage
"; + GH("etatActions",S); + } + myActionTimeout=setTimeout('EtatActions(0,0);',3500); + } + + }; + xhttp.open('GET', 'ajax_etatActions?Force=' +Force + '&NumAction=' + NumAction, true); + xhttp.send(); + } + + function LaVal(d){ + d=parseInt(d); + d=' '+d.toString(); + return d.substr(-9,3)+' '+d.substr(-6,3)+' '+d.substr(-3,3); + } + function Force(NumAction,Force){ + if (myActionTimeout !=null) clearTimeout(myActionTimeout); + EtatActions(Force,NumAction); + } + + function AdaptationSource(){ + var d='none'; + if(biSonde){ + d="table-cell"; + } + const collection = document.getElementsByClassName('dispT'); + for (let i = 0; i < collection.length; i++) { + collection[i].style.display = d; + } + + var S='Source : ' + if(Source=="Ext"){ + S +='ESP distant '+int2ip(RMSextIP); + GID("donneeDistante").style.display="block"; + }else { + S +='ESP local'; + } + GH('source',S); + } +)===="; diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlOTA.h b/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlOTA.h new file mode 100644 index 0000000..670dc66 --- /dev/null +++ b/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlOTA.h @@ -0,0 +1,95 @@ +//*************************************************** +// Page HTML et Javascript mise à jour du code par OTA +//*************************************************** +const char *OtaHtml = R"====( + + + + + + + + +

Web OTA

+

Mise à jour par Wifi

+
+ Votre version actuelle du routeur : +
+
+
+ Version(s) disponible(s) : +
+
+ +
+
+
    +
  • 1 - Téléchargez sur votre ordinateur, la version binaire du logiciel du routeur souhaitée (Solar_Router_Vxx.xx.ino.bin)
  • +
  • 2 - Cliquez sur "Choisir un fichier" et sélectionnez ce binaire sur votre ordinateur
  • +
  • 3 - Cliquez sur "Mettre à jour"
  • +
+
+
+ + +
+
progression: 0%
+ +
+
Routeur Version :
+
+ + + )===="; \ No newline at end of file diff --git a/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlPara.h b/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlPara.h new file mode 100644 index 0000000..310e959 --- /dev/null +++ b/docs/routers/F1ATB/Solar_Router_V10.00/pageHtmlPara.h @@ -0,0 +1,449 @@ +//*************************************************** +// Page HTML et Javascript de gestion des Paramètres +//*************************************************** +const char *ParaHtml = R"====( + + + + + + + +

Routeur Solaire - RMS

Paramètres

+
+
Source des mesures de puissance
+
+
+ + + + + + + + + + + + + + + + + + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
Nécessite un Reset de l'ESP32
+
+
+
+
Source des mesures de température
+
+
+ + + + + + + + +
+
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
Routeur
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + Nécessite un Reset de l'ESP32 +
+
+
+
+ + + + + + +
+
+
+
+ + + + + + +
+
+
+
+
Adresse IP de l'ESP32 du Routeur
+
+
+ + Nécessite un Reset de l'ESP32 +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
Paramètres serveur MQTT (Home Assistant , Domoticz ...)
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
Calibration Mesures Ueff et Ieff
+
+
+ + +
+
+ + +
+
+
+
+
+
+ +
+
+ +
+ +)===="; +const char *ParaJS = R"====( + var LaTemperature = -100; + function Init(){ + LoadParametres(); + LoadParaRouteur(); + } + function LoadParametres() { + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (this.readyState == 4 && this.status == 200) { + var LesParas=this.responseText; + var Para=LesParas.split(RS); + GID("dhcp").checked = Para[0]==1 ? true:false; + GID("adrIP").value=int2ip(Para[1]); + GID("gateway").value=int2ip(Para[2]); + GID("masque").value=int2ip(Para[3]); + GID("dns").value=int2ip(Para[4]); + GID(Para[5]).checked = true; + GID("RMSextIP").value=int2ip(Para[6]); + GID("EnphaseUser").value=Para[7]; + GID("EnphasePwd").value=Para[8]; + GID("EnphaseSerial").value=Para[9]; + GID("TopicP").value=Para[10]; + GID("MQTTRepete").value = Para[11]; + GID("MQTTIP").value=int2ip(Para[12]); + GID("MQTTPort").value=Para[13]; + GID("MQTTUser").value=Para[14]; + GID("MQTTpwd").value=Para[15]; + GID("MQTTPrefix").value=Para[16]; + GID("MQTTdeviceName").value=Para[17]; + GID("subMQTT").checked = Para[18]==1 ? true:false; + GID("nomRouteur").value=Para[19]; + GID("nomSondeFixe").value=Para[20]; + GID("nomSondeMobile").value=Para[21]; + LaTemperature=parseInt(Para[22]); + GID("nomTemperature").value=Para[23]; + GID(Para[24]).checked = true; + GID("TopicT").value=Para[25]; + GID("IPtemp").value=int2ip(Para[26]); + GID("CalibU").value=Para[27]; + GID("CalibI").value=Para[28]; + GID("TempoEDFon").checked = Para[29]==1 ? true:false; + GID("WifiSleep").checked = Para[30]==1 ? true:false; + GID("Serial" + Para[31]).checked = true; + GID("Triac" + Para[32]).checked = true; + + checkDisabled(); + } + }; + xhttp.open('GET', 'ParaAjax', true); + xhttp.send(); + } + function SendValues(){ + GID("attente").style="visibility: visible;"; + var dhcp = GID("dhcp").checked ? 1:0; + var TempoEDFon = GID("TempoEDFon").checked ? 1:0; + var Source_new = document.querySelector('input[name="sources"]:checked').value; + var Source_Temp = document.querySelector('input[name="srcTemp"]:checked').value; + var subMQTT = GID("subMQTT").checked ? 1:0; + var WifiSleep = GID("WifiSleep").checked ? 1:0; + var Serial = document.querySelector('input[name="pSerie"]:checked').value; + var pTriac = document.querySelector('input[name="pTriac"]:checked').value; + var S=dhcp+RS+ ip2int(GID("adrIP").value)+RS+ ip2int(GID("gateway").value); + S +=RS+ip2int(GID("masque").value)+RS+ ip2int(GID("dns").value) + S +=RS+Source_new+RS+ ip2int(GID("RMSextIP").value)+ RS+GID("EnphaseUser").value.trim()+RS+GID("EnphasePwd").value.trim()+RS+GID("EnphaseSerial").value.trim() +RS+GID("TopicP").value.trim(); + S +=RS+GID("MQTTRepete").value +RS+ip2int(GID("MQTTIP").value) +RS+GID("MQTTPort").value +RS+GID("MQTTUser").value.trim()+RS+GID("MQTTpwd").value.trim(); + S +=RS+GID("MQTTPrefix").value.trim()+RS+GID("MQTTdeviceName").value.trim() + RS + subMQTT; + S +=RS+GID("nomRouteur").value.trim()+RS+GID("nomSondeFixe").value.trim()+RS+GID("nomSondeMobile").value.trim(); + S +=RS+GID("nomTemperature").value.trim() +RS+Source_Temp +RS+GID("TopicT").value.trim() + RS + ip2int(GID("IPtemp").value); + S +=RS+GID("CalibU").value+RS+GID("CalibI").value + RS + TempoEDFon + RS + WifiSleep + RS + Serial + RS + pTriac; + S="?lesparas="+clean(S); + if ((GID("dhcp").checked || checkIP("adrIP")&&checkIP("gateway")) && (!GID("MQTTRepete").checked || checkIP("MQTTIP"))){ + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (this.readyState == 4 && this.status == 200) { + var retour=this.responseText; + location.reload(); + } + }; + xhttp.open('GET', 'ParaUpdate'+S, true); + xhttp.send(); + } + } + function checkDisabled(){ + GID("infoIP").style.display = GID("dhcp").checked ? "none" : "table"; + GID("Zmqtt").style.display = (GID("MQTTRepete").value != 0 || GID("Pmqtt").checked || GID("tempMqtt").checked || GID("subMQTT").checked) ? "table" : "none"; + GID('ligneTemperature').style.display = (GID("tempNo").checked) ? "none" : "table"; + GID('ligneTopicT').style.display = (GID("tempMqtt").checked) ? "table-row" : "none"; + GID('ligneIPtemp').style.display = (GID("tempExt").checked) ? "table-row" : "none"; + GID('ligneTopicP').style.display = (GID("Pmqtt").checked) ? "table-row" : "none"; + Source = document.querySelector('input[name="sources"]:checked').value; + if (Source !='Ext') Source_data=Source; + AdaptationSource(); + } + function checkIP(id){ + var S=GID(id).value; + var Table=S.split("."); + var valide=true; + if (Table.length!=4) { + valide=false; + }else{ + for (var i=0;i255 || Table[i]<0) valide=false; + } + } + if (valide){ + GID(id).style.color="black"; + } else { + GID(id).style.color="red"; + } + return valide; + } + + + function Reset(){ + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (this.readyState == 4 && this.status == 200) { + GID('BoutonsBas').innerHTML=this.responseText; + setTimeout(location.reload(),3000); + } + }; + xhttp.open('GET', 'restart', true); + xhttp.send(); + } + function AdaptationSource(){ + GID('ligneFixe').style.display = (Source_data=='UxIx2' || (Source_data=='ShellyEm' && GID("EnphaseSerial").value <3))? "table-row" : "none"; + GID('Zcalib').style.display=(Source_data=='UxI' && Source=='UxI' ) ? "table" : "none"; + var txtExt = "ESP-RMS"; + if (Source=='Enphase') txtExt = "Enphase-Envoy"; + if (Source=='SmartG') txtExt = "SmartGateways"; + var lab_enphaseShelly= "Numéro série passerelle IQ Enphase :
Pour firmvare Envoy-S V7 seulement
"; + if (Source=='ShellyEm') { + txtExt = "Shelly Em "; + lab_enphaseShelly="Monophasé : Numéro de voie (0 ou 1) mesurant l'entrée du courant maison
Triphasé : mettre 3"; + } + GID('labExtIp').innerHTML = txtExt; + GID('label_enphase_shelly').innerHTML = lab_enphaseShelly; + GID('ligneExt').style.display = (Source=='Ext' || Source=='Enphase' || Source=='SmartG' || Source=='ShellyEm') ? "table-row" : "none"; + GID('ligneEnphaseUser').style.display = (Source=='Enphase') ? "table-row" : "none"; + GID('ligneEnphasePwd').style.display = (Source=='Enphase') ? "table-row" : "none"; + GID('ligneEnphaseSerial').style.display = (Source=='Enphase' || Source=='ShellyEm') ? "table-row" : "none"; //Numéro de serie ou voie + } +)===="; + +//Paramètres du routeur et fonctions générales pour toutes les pages. +const char *ParaRouteurJS = R"====( + var Source=""; + var Source_data=""; + var RMSextIP=""; + var GS=String.fromCharCode(29); //Group Separator + var RS=String.fromCharCode(30); //Record Separator + var nomSondeFixe="Sonde Fixe"; + var nomSondeMobile="Sonde Mobile"; + var nomTemperature="Temperature"; + function LoadParaRouteur() { + var xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function() { + if (this.readyState == 4 && this.status == 200) { + var LesParas=this.responseText; + var Para=LesParas.split(RS); + Source=Para[0]; + Source_data=Para[1]; + RMSextIP= Para[6]; + AdaptationSource(); + GH("nom_R",Para[2]); + GH("version",Para[3]); + GH("nomSondeFixe",Para[4]); + GH("nomSondeMobile",Para[5]); + nomSondeFixe=Para[4]; + nomSondeMobile=Para[5]; + nomTemperature=Para[7]; + + } + }; + xhttp.open('GET', 'ParaRouteurAjax', true); + xhttp.send(); + } + function GID(id) { return document.getElementById(id); }; + function GH(id, T) { + if ( GID(id)){ + GID(id).innerHTML = T; } + } + function GV(id, T) { GID(id).value = T; } + function clean(S){ //Remplace & et ? pour les envois au serveur + let res=S.replace(/\%/g,"%25"); + res = res.replace(/\&/g, "%26"); + res = res.replace(/\#/g, "%23"); + res = res.replace(/\+/g, "%2B"); + res=res.replace(/amp;/g,""); + return res.replace(/\?/g,"%3F"); + } + function int2ip (V) { + var ipInt=parseInt(V); + return ( (ipInt>>>24) +'.' + (ipInt>>16 & 255) +'.' + (ipInt>>8 & 255) +'.' + (ipInt & 255) ); + } + function ip2int(ip) { + ip=ip.trim(); + return ip.split('.').reduce(function(ipInt, octet) { return (ipInt<<8) + parseInt(octet, 10)}, 0) >>> 0; + } + +)===="; \ No newline at end of file diff --git "a/docs/routers/F1ATB/Triacs gradateurs pour routeur photovolta\303\257que \342\200\223 F1ATB.pdf" "b/docs/routers/F1ATB/Triacs gradateurs pour routeur photovolta\303\257que \342\200\223 F1ATB.pdf" new file mode 100644 index 0000000..6c612f5 Binary files /dev/null and "b/docs/routers/F1ATB/Triacs gradateurs pour routeur photovolta\303\257que \342\200\223 F1ATB.pdf" differ diff --git a/docs/routers/Florent/triac_timer/triac_timer.cpp b/docs/routers/Florent/triac_timer/triac_timer.cpp new file mode 100644 index 0000000..ccef097 --- /dev/null +++ b/docs/routers/Florent/triac_timer/triac_timer.cpp @@ -0,0 +1,375 @@ +#include "triac_timer.hpp" + +#include "driver/gpio.h" +#include "hal/gpio_hal.h" +#include "hal/gpio_ll.h" +#include "hal/gpio_types.h" +#include "soc/gpio_reg.h" + +#include "driver/timer.h" +#include "hal/timer_ll.h" +#include "soc/timer_group_reg.h" + +#include "esp_check.h" +#include "hal/clk_tree_hal.h" +#include "rom/ets_sys.h" +#include "soc/soc.h" + + +static const char* TAG = "triac-timer"; + + +// Triac pulse width, microseconds. +#define PULSE_WIDTH_US ( 400 ) + +// Minimum delay to reach the voltage required for a gate current of 30mA. +// delay_us = asin((gate_resistor * gate_current) / grid_volt_max) / pi * period_us +// delay_us = asin((330 * 0.03) / 325) / pi * 10000 = 97us +#define PHASE_DELAY_MIN_US ( 90 ) + +// Minimum time to lock-out the zero crossing once it is triggered, microseconds. +// This is to avoid interrupts on the opposite edge when there's a significant slew rate. +#define ZC_FILTER_US ( 2000 ) + +// Hardware timer +static_assert(CONFIG_TRIAC_TIMER_NUM <= SOC_TIMER_GROUP_TOTAL_TIMERS); +#define TIMER_GRP ( CONFIG_TRIAC_TIMER_NUM / SOC_TIMER_GROUPS ) +#define TIMER_IDX ( CONFIG_TRIAC_TIMER_NUM % SOC_TIMER_GROUP_TIMERS_PER_GROUP ) + + +/* + * Low level API similar to "hal/gpio_ll.h" and "hal/timer_ll.h" + */ + +#define INLINE_ATTR static inline __attribute__((always_inline)) + +INLINE_ATTR void _gpio_ll_clear_outputs(uint64_t mask) { + REG_WRITE(GPIO_OUT_W1TC_REG, (uint32_t)mask); // GPIO0~31 + #if SOC_GPIO_PIN_COUNT > 31 + if ((uint32_t)(mask >> 32)) + REG_WRITE(GPIO_OUT1_W1TC_REG, (uint32_t)(mask >> 32)); // GPIO32~39 + #endif +} + +INLINE_ATTR void _gpio_ll_set_outputs(uint64_t mask) { + REG_WRITE(GPIO_OUT_W1TS_REG, (uint32_t)mask); // GPIO0~31 + #if SOC_GPIO_PIN_COUNT > 31 + if ((uint32_t)(mask >> 32)) + REG_WRITE(GPIO_OUT1_W1TS_REG, (uint32_t)(mask >> 32)); // GPIO32~39 + #endif +} + + +#if TIMER_IDX == 0 + #define TIMG_LOAD_REG TIMG_T0LOAD_REG(TIMER_GRP) + #define TIMG_LOADHI_REG TIMG_T0LOADHI_REG(TIMER_GRP) + #define TIMG_LOADLO_REG TIMG_T0LOADLO_REG(TIMER_GRP) + #define TIMG_UPDATE_REG TIMG_T0UPDATE_REG(TIMER_GRP) + #define TIMG_LO_REG TIMG_T0LO_REG(TIMER_GRP) + #define TIMG_ALARMHI_REG TIMG_T0ALARMHI_REG(TIMER_GRP) + #define TIMG_ALARMLO_REG TIMG_T0ALARMLO_REG(TIMER_GRP) + #define TIMG_CONFIG_REG TIMG_T0CONFIG_REG(TIMER_GRP) + #define TIMG_INT_CLR TIMG_T0_INT_CLR +#else + #define TIMG_LOAD_REG TIMG_T1LOAD_REG(TIMER_GRP) + #define TIMG_LOADHI_REG TIMG_T1LOADHI_REG(TIMER_GRP) + #define TIMG_LOADLO_REG TIMG_T1LOADLO_REG(TIMER_GRP) + #define TIMG_UPDATE_REG TIMG_T1UPDATE_REG(TIMER_GRP) + #define TIMG_LO_REG TIMG_T1LO_REG(TIMER_GRP) + #define TIMG_ALARMHI_REG TIMG_T1ALARMHI_REG(TIMER_GRP) + #define TIMG_ALARMLO_REG TIMG_T1ALARMLO_REG(TIMER_GRP) + #define TIMG_CONFIG_REG TIMG_T1CONFIG_REG(TIMER_GRP) + #define TIMG_INT_CLR TIMG_T1_INT_CLR +#endif + +INLINE_ATTR void _timer_ll_clear_intr_status() { + REG_WRITE(TIMG_INT_CLR_TIMERS_REG(TIMER_GRP), TIMG_INT_CLR); +} + +INLINE_ATTR void _timer_ll_set_reload_value(uint32_t value) { + REG_WRITE(TIMG_LOADHI_REG, 0); + REG_WRITE(TIMG_LOADLO_REG, value); +} + +INLINE_ATTR void _timer_ll_set_alarm_value(uint32_t value) { + REG_WRITE(TIMG_ALARMHI_REG, 0); + REG_WRITE(TIMG_ALARMLO_REG, value); +} + +INLINE_ATTR void _timer_ll_reload() { + REG_WRITE(TIMG_LOAD_REG, 1); +} + +INLINE_ATTR uint32_t _timer_ll_get_value() { + REG_WRITE(TIMG_UPDATE_REG, 1); + // Spin wait for the update as implemented by the SDK. + // We only need the lower 32bits so the wait could be useless if the lower registry is updated first. + while (REG_READ(TIMG_UPDATE_REG)) { } + return REG_READ(TIMG_LO_REG); +} + +INLINE_ATTR void _timer_ll_set_alarm(uint32_t value) { + REG_WRITE(TIMG_ALARMLO_REG, value); + REG_SET_BIT(TIMG_CONFIG_REG, TIMG_T0_ALARM_EN | TIMG_T0_LEVEL_INT_EN); +} + +INLINE_ATTR uint32_t _timer_ll_get_alarm() { + return REG_READ(TIMG_ALARMLO_REG); +} + + +/** + * AC phase delay table (sine square inverse CDF). + * power_ratio = sin(2 * pi * delay_ratio) / (2 * pi) - delay_ratio + 1 + * index 0 ≈ 0% power ≈ 5% phase angle ≈ 95% firing delay + * index 39 ≈ 50% power ≈ 50% phase angle ≈ 50% firing delay + * index 79 ≈ 100% power ≈ 95% phase angle ≈ 5% firing delay + */ + +static_assert(CONFIG_TRIAC_RESOLUTION <= 15); + +#define TABLE_PHASE_LEN ( 80U ) +#define TABLE_PHASE_SCALE ( (TABLE_PHASE_LEN - 1U) * (1UL << (16 - CONFIG_TRIAC_RESOLUTION)) ) + +static const uint16_t TABLE_PHASE_DELAY[TABLE_PHASE_LEN] { + 0xefea, 0xdfd4, 0xd735, 0xd10d, 0xcc12, 0xc7cc, 0xc403, 0xc094, + 0xbd6a, 0xba78, 0xb7b2, 0xb512, 0xb291, 0xb02b, 0xaddc, 0xaba2, + 0xa97a, 0xa762, 0xa557, 0xa35a, 0xa167, 0x9f7f, 0x9da0, 0x9bc9, + 0x99fa, 0x9831, 0x966e, 0x94b1, 0x92f9, 0x9145, 0x8f95, 0x8de8, + 0x8c3e, 0x8a97, 0x88f2, 0x8750, 0x85ae, 0x840e, 0x826e, 0x80cf, + 0x7f31, 0x7d92, 0x7bf2, 0x7a52, 0x78b0, 0x770e, 0x7569, 0x73c2, + 0x7218, 0x706b, 0x6ebb, 0x6d07, 0x6b4f, 0x6992, 0x67cf, 0x6606, + 0x6437, 0x6260, 0x6081, 0x5e99, 0x5ca6, 0x5aa9, 0x589e, 0x5686, + 0x545e, 0x5224, 0x4fd5, 0x4d6f, 0x4aee, 0x484e, 0x4588, 0x4296, + 0x3f6c, 0x3bfd, 0x3834, 0x33ee, 0x2ef3, 0x28cb, 0x202c, 0x1016}; + + +static uint16_t lookup_phase_delay(uint32_t duty, uint16_t period) +{ + uint32_t slot = duty * TABLE_PHASE_SCALE + (TABLE_PHASE_SCALE >> 1); + uint16_t index = slot >> 16; + uint32_t a = TABLE_PHASE_DELAY[index]; + uint32_t b = TABLE_PHASE_DELAY[index + 1]; + uint32_t delay = a - (((a - b) * (slot & 0xffff)) >> 16); // interpolate a b + + return (delay * period) >> 16; // scale to period +} + + + +static Triac* _triac_root = NULL; // instance chaining +static uint64_t _out_mask = 0; // mask for all the output pins +static gpio_num_t _zc_pin = GPIO_NUM_NC; // zero crossing pin number +static uint32_t _zc_delay = 0; // zero crossing delay, unit: 1us +static volatile uint32_t _zc_period = 0; // zero crossing period, unit: 1us << 16 + + +static void IRAM_ATTR __isr_crossing(void *arg) +{ + uint32_t tick_now = _timer_ll_get_value(); + uint32_t tick_next = -1; + + // suspend zero-crossing interrupts to lock-out the ZC input + // uint32_t tick_next = ZC_FILTER_US; + // gpio_ll_set_intr_type(&GPIO, _zc_pin, GPIO_INTR_DISABLE); + + if (tick_now > ZC_FILTER_US) // lock-out interval to avoid interrupts on the opposite edge + { + // reset timer and clear interrupts in case some are pending + _timer_ll_reload(); + _timer_ll_clear_intr_status(); + + // turn off outputs in case some are still ON + _gpio_ll_clear_outputs(_out_mask); + + // set each triac firing delay and pick the lowest for the alarm + for (Triac* triac = _triac_root; triac; triac = triac->_next) + { + uint32_t delay = triac->_delay; + triac->_alarm = delay; + triac->_triggered = false; + + if (delay < tick_next) + tick_next = delay; + } + + // set alarm + if ((int32_t)tick_next >= 0) { + _timer_ll_set_alarm(tick_next); + } + + // update period, Exponential Moving Average N=16 + int32_t avg = _zc_period; // 1us << 16 + avg += ((int32_t)(tick_now << 16) - avg) >> 4; + _zc_period = avg; + } +} + + +static void IRAM_ATTR __isr_timer(void *arg) +{ + // Clear the interrupt (re-entrance occur when called at the end). + _timer_ll_clear_intr_status(); + + // Get elapsed time since zero crossing. Can use the timer value or alarm value. + // The alarm value is cheaper but less accurate if the interrupt is delayed. + uint32_t tick_now = _timer_ll_get_value(); + // uint32_t tick_now = _timer_ll_get_alarm(); + uint32_t tick_next = -1; + + // ets_printf("%lu\n", tick_now); + + // handle triacs with a lower alarm + for (Triac* triac = _triac_root; triac; triac = triac->_next) + { + uint32_t tick = triac->_alarm; // pulse ON or OFF delay from ZC + + if (tick <= tick_now) { + // turn OFF pulse if ON + if (triac->_triggered) { + _gpio_ll_clear_outputs(triac->_mask); + triac->_alarm = -1; // turn off triac alarm + continue; + } + // turn ON pulse + _gpio_ll_set_outputs(triac->_mask); + tick = tick_now + PULSE_WIDTH_US; // add pulse width + triac->_triggered = true; // triggered flag + triac->_alarm = tick; // triac next alarm + } + + // pick lowest tick for next alarm + if (tick < tick_next) { + tick_next = tick; + } + } + + // set alarm, works even when set before the timer current value. + if ((int32_t)tick_next >= 0) { + _timer_ll_set_alarm(tick_next); + } + + // restore zero-crossing interrupts + // gpio_ll_set_intr_type(&GPIO, _zc_pin, GPIO_INTR_POSEDGE); +} + + +bool Triac::begin(gpio_num_t sync_pin, uint16_t delay_us, bool invert) +{ + _zc_delay = delay_us; + _zc_pin = sync_pin; + + // output pins + + for (Triac* triac = _triac_root; triac; triac = triac->_next) + { + ESP_ERROR_CHECK(gpio_set_level(triac->_pin, 0)); + ESP_ERROR_CHECK(gpio_reset_pin(triac->_pin)); + ESP_ERROR_CHECK(gpio_set_direction(triac->_pin, GPIO_MODE_OUTPUT)); + } + + // general purpose timer + + timer_config_t timer_cfg = { }; + timer_cfg.alarm_en = TIMER_ALARM_DIS; + timer_cfg.counter_en = TIMER_START; + timer_cfg.intr_type = TIMER_INTR_LEVEL; + timer_cfg.counter_dir = TIMER_COUNT_UP; + timer_cfg.auto_reload = TIMER_AUTORELOAD_DIS; + timer_cfg.clk_src = TIMER_SRC_CLK_DEFAULT; + timer_cfg.divider = clk_hal_apb_get_freq_hz() / 1000000; // 1us tick + + ESP_ERROR_CHECK(timer_init((timer_group_t)TIMER_GRP, + (timer_idx_t)TIMER_IDX, + &timer_cfg)); + + ESP_ERROR_CHECK(timer_isr_register((timer_group_t)TIMER_GRP, + (timer_idx_t)TIMER_IDX, + __isr_timer, + NULL, + ESP_INTR_FLAG_LOWMED | ESP_INTR_FLAG_IRAM, + NULL)); + + ESP_ERROR_CHECK(timer_enable_intr((timer_group_t)TIMER_GRP, + (timer_idx_t)TIMER_IDX)); + + _timer_ll_set_reload_value(0); + _timer_ll_set_alarm_value(0); + + // zero-crossing interrupt + + ESP_ERROR_CHECK(gpio_install_isr_service(ESP_INTR_FLAG_LOWMED | ESP_INTR_FLAG_IRAM)); + + gpio_config_t io_cfg = { }; + io_cfg.intr_type = invert ? GPIO_INTR_NEGEDGE : GPIO_INTR_POSEDGE; + io_cfg.mode = GPIO_MODE_INPUT; + io_cfg.pin_bit_mask = (1ULL << sync_pin); + io_cfg.pull_down_en = GPIO_PULLDOWN_DISABLE; + io_cfg.pull_up_en = GPIO_PULLUP_ENABLE; + ESP_ERROR_CHECK(gpio_config(&io_cfg)); + + ESP_ERROR_CHECK(gpio_isr_handler_add(sync_pin, __isr_crossing, NULL)); + + // esp_intr_dump(NULL); + + return true; +} + + +void Triac::end() +{ + gpio_isr_handler_remove(_zc_pin); + timer_deinit((timer_group_t)TIMER_GRP, (timer_idx_t)TIMER_IDX); + _gpio_ll_clear_outputs(_out_mask); // turn off triacs +} + + +uint32_t Triac::get_period_us() +{ + return (_zc_period + (1U << 14)) >> 15; // shift 16, x2, round +} + + + +Triac::Triac(gpio_num_t pin) : + _next(_triac_root), // chain next instance + _pin(pin), // output pin number + _mask(1ULL << pin), // output pin mask + _delay(-1), // triac firing tick + _alarm(-1), // next tick for pulse start or pulse end + _triggered(false) // triggered flag +{ + _triac_root = this; // chain as root + _out_mask |= _mask; // add to outputs mask +} + + +void Triac::set(int32_t value) +{ + if (value <= 0) { + _delay = UINT32_MAX; // OFF 0% + this->value = 0; + } + else if (value >= TRIAC_MAX) { + _delay = _zc_delay + PHASE_DELAY_MIN_US; // ON 100% + this->value = TRIAC_MAX; + } + else { + _delay = _zc_delay + lookup_phase_delay(value, _zc_period >> 16); // dimmed + this->value = value; + } +} + + +int32_t Triac::add(int32_t value) +{ + int32_t v = this->value; + set(v + value); + return this->value - v; +} + + +void Triac::test_zc() +{ + _delay = _zc_delay; +} diff --git a/docs/routers/Florent/triac_timer/triac_timer.hpp b/docs/routers/Florent/triac_timer/triac_timer.hpp new file mode 100644 index 0000000..c12a497 --- /dev/null +++ b/docs/routers/Florent/triac_timer/triac_timer.hpp @@ -0,0 +1,141 @@ +/** + * Low level phase control driver implemented with a Timer Group (TIMG). + * + * 12 bits resolution by default (0 to 4095). + * Detects automatically the frequency of the grid. + * The output power is linear (sine square distribution). + * No limitation on the number of output. + * The zero crossing input is filtered. + * Designed for minimum CPU usage and low latency. + * Uses integers only, no floating points, no FPU lock. + * + * Drawbacks: + * Interrupts adds a 5us delay between ZC input and output. + * Interrupts may be delayed under heavy load. + * + * Usage: + * + * #include "triac-timer.hpp" + * + * #define PIN_ZEROCROSS ( 16 ) + * #define PIN_TRIAC_1 ( 17 ) + * #define PIN_TRIAC_2 ( 18 ) + * #define ZC_DELAY_US ( 170 ) + * + * Triac triac1(PIN_TRIAC_1); + * Triac triac2(PIN_TRIAC_2); + * + * void setup() + * { + * Triac::begin(PIN_ZEROCROSS, ZC_DELAY_US); + * } + * + * void loop() + * { + * triac1.set(100 * TRIAC_MAX / 100); // 100% + * triac2.set(75 * TRIAC_MAX / 100); // 75% + * delay(500); + * triac1.set(0 * TRIAC_MAX / 100); // 0% + * triac2.set(25 * TRIAC_MAX / 100); // 25% + * delay(500); + * } + * + * TODO: + * check interrupt priority gpio >= timer and occur on same core + * check watchdog timer + * + */ + +#pragma once + +#include "hal/gpio_types.h" +#include +#include + +/** + * Optional timer, 0-3 for most ESP32, 0-1 for ESP32-C3 + */ +#ifndef CONFIG_TRIAC_TIMER_NUM +#define CONFIG_TRIAC_TIMER_NUM ( 0 ) +#endif + +/** + * Optional resolution, 15bits max + */ +#ifndef CONFIG_TRIAC_RESOLUTION +#define CONFIG_TRIAC_RESOLUTION ( 12 ) +#endif + + +#define TRIAC_MAX ( (1 << CONFIG_TRIAC_RESOLUTION) - 1 ) + + +struct Triac +{ + Triac* _next; // next triac instance + gpio_num_t _pin; // output pin number + uint64_t _mask; // output mask + uint32_t _delay; // triac firing delay, us + + volatile uint32_t _alarm; // alarm tick for pulse start or pulse end + volatile bool _triggered; // ON/OFF flag + + uint32_t value; // 12 bits duty, 0-4095, readonly + + + /** + * @brief Constructor + * + * @param pin Output pin number + */ + Triac(gpio_num_t pin); + + /** + * @brief Set the output duty/power. + * Must wait at least 2 seconds after calling Triac::begin + * + * @param value 12 bits from 0 (OFF) to 4095 (Fully ON). + */ + void set(int32_t value); + + /** + * @brief Increase or decrease the output duty/power. + * + * @param value 12 bits, +/-4095 (+/-100%) + * @return Added power within available range. + */ + int32_t add(int32_t value); + + /** + * @brief Outputs the zero crossing signal for calibration. + */ + void test_zc(); + + + + /** + * @brief Configure and start the phase controller. + * + * @param sync_pin Zero crossing input pins, GPIO number. + * @param delay_us Zero crossing delay, microseconds. + * @param invert Invert the zero crossing input, false: high pulse, true: low pulse. + */ + static bool begin(gpio_num_t sync_pin, uint16_t delay_us = 170, bool invert = false); + + /** + * @brief Stop the phase controller and free resources. + */ + static void end(); + + /** + * @brief Grid period, moving average, microsecond. + * + * @example + * + * uint32_t period_us = Triac::get_period_us(); + * float frequency_hz = period_us ? (1000000.0 / period_us) : 0; + * + */ + static uint32_t get_period_us(); + +}; diff --git a/docs/routers/FredM67-PVRouter-1-phase b/docs/routers/FredM67-PVRouter-1-phase new file mode 160000 index 0000000..9678c01 --- /dev/null +++ b/docs/routers/FredM67-PVRouter-1-phase @@ -0,0 +1 @@ +Subproject commit 9678c018e0bb8af5e19d021baf8d94aff3d555a9 diff --git a/docs/routers/FredM67-PVRouter-3-phase b/docs/routers/FredM67-PVRouter-3-phase new file mode 160000 index 0000000..42da896 --- /dev/null +++ b/docs/routers/FredM67-PVRouter-3-phase @@ -0,0 +1 @@ +Subproject commit 42da89637e99a66feb2e88df09ba4c0deb0885c3 diff --git a/docs/routers/Jetblack31-MaxPV b/docs/routers/Jetblack31-MaxPV new file mode 160000 index 0000000..9db686b --- /dev/null +++ b/docs/routers/Jetblack31-MaxPV @@ -0,0 +1 @@ +Subproject commit 9db686b093b7d4527a27ecd87d3f88c320d4af9a diff --git a/docs/routers/LSA/Explication Solar PID_fr.pdf b/docs/routers/LSA/Explication Solar PID_fr.pdf new file mode 100644 index 0000000..713ea4d Binary files /dev/null and b/docs/routers/LSA/Explication Solar PID_fr.pdf differ diff --git "a/docs/routers/LSA/Optimiseur autoadapt zero injection PID construire soi-m\303\252me instructions - Forum photovolta\303\257que.pdf" "b/docs/routers/LSA/Optimiseur autoadapt zero injection PID construire soi-m\303\252me instructions - Forum photovolta\303\257que.pdf" new file mode 100644 index 0000000..6ee5eac Binary files /dev/null and "b/docs/routers/LSA/Optimiseur autoadapt zero injection PID construire soi-m\303\252me instructions - Forum photovolta\303\257que.pdf" differ diff --git a/docs/routers/LSA/Programmation_ESP32.pdf b/docs/routers/LSA/Programmation_ESP32.pdf new file mode 100644 index 0000000..b295880 Binary files /dev/null and b/docs/routers/LSA/Programmation_ESP32.pdf differ diff --git a/docs/routers/LSA/Regler_pwm_will/.gitignore b/docs/routers/LSA/Regler_pwm_will/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/docs/routers/LSA/Regler_pwm_will/data/Kd.txt b/docs/routers/LSA/Regler_pwm_will/data/Kd.txt new file mode 100644 index 0000000..7d385d4 --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/Kd.txt @@ -0,0 +1 @@ +0.25 diff --git a/docs/routers/LSA/Regler_pwm_will/data/Kd_1.txt b/docs/routers/LSA/Regler_pwm_will/data/Kd_1.txt new file mode 100644 index 0000000..7d385d4 --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/Kd_1.txt @@ -0,0 +1 @@ +0.25 diff --git a/docs/routers/LSA/Regler_pwm_will/data/Kd_2.txt b/docs/routers/LSA/Regler_pwm_will/data/Kd_2.txt new file mode 100644 index 0000000..7d385d4 --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/Kd_2.txt @@ -0,0 +1 @@ +0.25 diff --git a/docs/routers/LSA/Regler_pwm_will/data/Ki.txt b/docs/routers/LSA/Regler_pwm_will/data/Ki.txt new file mode 100644 index 0000000..e9b8f99 --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/Ki.txt @@ -0,0 +1 @@ +0.05 diff --git a/docs/routers/LSA/Regler_pwm_will/data/Ki_1.txt b/docs/routers/LSA/Regler_pwm_will/data/Ki_1.txt new file mode 100644 index 0000000..e9b8f99 --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/Ki_1.txt @@ -0,0 +1 @@ +0.05 diff --git a/docs/routers/LSA/Regler_pwm_will/data/Ki_2.txt b/docs/routers/LSA/Regler_pwm_will/data/Ki_2.txt new file mode 100644 index 0000000..3b04cfb --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/Ki_2.txt @@ -0,0 +1 @@ +0.2 diff --git a/docs/routers/LSA/Regler_pwm_will/data/Kp.txt b/docs/routers/LSA/Regler_pwm_will/data/Kp.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/Kp.txt @@ -0,0 +1 @@ +1 diff --git a/docs/routers/LSA/Regler_pwm_will/data/Kp_1.txt b/docs/routers/LSA/Regler_pwm_will/data/Kp_1.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/Kp_1.txt @@ -0,0 +1 @@ +1 diff --git a/docs/routers/LSA/Regler_pwm_will/data/Kp_2.txt b/docs/routers/LSA/Regler_pwm_will/data/Kp_2.txt new file mode 100644 index 0000000..49d5957 --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/Kp_2.txt @@ -0,0 +1 @@ +0.1 diff --git a/docs/routers/LSA/Regler_pwm_will/data/PWM_Frequency.txt b/docs/routers/LSA/Regler_pwm_will/data/PWM_Frequency.txt new file mode 100644 index 0000000..83b33d2 --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/PWM_Frequency.txt @@ -0,0 +1 @@ +1000 diff --git a/docs/routers/LSA/Regler_pwm_will/data/PWM_Resolution.txt b/docs/routers/LSA/Regler_pwm_will/data/PWM_Resolution.txt new file mode 100644 index 0000000..3cacc0b --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/PWM_Resolution.txt @@ -0,0 +1 @@ +12 \ No newline at end of file diff --git a/docs/routers/LSA/Regler_pwm_will/data/SSR_1_PID_direction.txt b/docs/routers/LSA/Regler_pwm_will/data/SSR_1_PID_direction.txt new file mode 100644 index 0000000..56a6051 --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/SSR_1_PID_direction.txt @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/docs/routers/LSA/Regler_pwm_will/data/SSR_1_mode.txt b/docs/routers/LSA/Regler_pwm_will/data/SSR_1_mode.txt new file mode 100644 index 0000000..0cfbf08 --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/SSR_1_mode.txt @@ -0,0 +1 @@ +2 diff --git a/docs/routers/LSA/Regler_pwm_will/data/SSR_1_off.txt b/docs/routers/LSA/Regler_pwm_will/data/SSR_1_off.txt new file mode 100644 index 0000000..573541a --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/SSR_1_off.txt @@ -0,0 +1 @@ +0 diff --git a/docs/routers/LSA/Regler_pwm_will/data/SSR_1_on.txt b/docs/routers/LSA/Regler_pwm_will/data/SSR_1_on.txt new file mode 100644 index 0000000..64bb6b7 --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/SSR_1_on.txt @@ -0,0 +1 @@ +30 diff --git a/docs/routers/LSA/Regler_pwm_will/data/SSR_1_setpoint_distance.txt b/docs/routers/LSA/Regler_pwm_will/data/SSR_1_setpoint_distance.txt new file mode 100644 index 0000000..abdfb05 --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/SSR_1_setpoint_distance.txt @@ -0,0 +1 @@ +60 diff --git a/docs/routers/LSA/Regler_pwm_will/data/SSR_2_mode.txt b/docs/routers/LSA/Regler_pwm_will/data/SSR_2_mode.txt new file mode 100644 index 0000000..573541a --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/SSR_2_mode.txt @@ -0,0 +1 @@ +0 diff --git a/docs/routers/LSA/Regler_pwm_will/data/SSR_2_off.txt b/docs/routers/LSA/Regler_pwm_will/data/SSR_2_off.txt new file mode 100644 index 0000000..573541a --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/SSR_2_off.txt @@ -0,0 +1 @@ +0 diff --git a/docs/routers/LSA/Regler_pwm_will/data/SSR_2_on.txt b/docs/routers/LSA/Regler_pwm_will/data/SSR_2_on.txt new file mode 100644 index 0000000..f599e28 --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/SSR_2_on.txt @@ -0,0 +1 @@ +10 diff --git a/docs/routers/LSA/Regler_pwm_will/data/Setpoint.txt b/docs/routers/LSA/Regler_pwm_will/data/Setpoint.txt new file mode 100644 index 0000000..209e3ef --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/Setpoint.txt @@ -0,0 +1 @@ +20 diff --git a/docs/routers/LSA/Regler_pwm_will/data/Setpoint_1.txt b/docs/routers/LSA/Regler_pwm_will/data/Setpoint_1.txt new file mode 100644 index 0000000..209e3ef --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/Setpoint_1.txt @@ -0,0 +1 @@ +20 diff --git a/docs/routers/LSA/Regler_pwm_will/data/Setpoint_2.txt b/docs/routers/LSA/Regler_pwm_will/data/Setpoint_2.txt new file mode 100644 index 0000000..209e3ef --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/Setpoint_2.txt @@ -0,0 +1 @@ +20 diff --git a/docs/routers/LSA/Regler_pwm_will/data/activate_1.txt b/docs/routers/LSA/Regler_pwm_will/data/activate_1.txt new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/activate_1.txt @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/docs/routers/LSA/Regler_pwm_will/data/activate_2.txt b/docs/routers/LSA/Regler_pwm_will/data/activate_2.txt new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/activate_2.txt @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/docs/routers/LSA/Regler_pwm_will/data/aggKd.txt b/docs/routers/LSA/Regler_pwm_will/data/aggKd.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/aggKd.txt @@ -0,0 +1 @@ +1 diff --git a/docs/routers/LSA/Regler_pwm_will/data/aggKd_1.txt b/docs/routers/LSA/Regler_pwm_will/data/aggKd_1.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/aggKd_1.txt @@ -0,0 +1 @@ +1 diff --git a/docs/routers/LSA/Regler_pwm_will/data/aggKd_2.txt b/docs/routers/LSA/Regler_pwm_will/data/aggKd_2.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/aggKd_2.txt @@ -0,0 +1 @@ +1 diff --git a/docs/routers/LSA/Regler_pwm_will/data/aggKi.txt b/docs/routers/LSA/Regler_pwm_will/data/aggKi.txt new file mode 100644 index 0000000..3b04cfb --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/aggKi.txt @@ -0,0 +1 @@ +0.2 diff --git a/docs/routers/LSA/Regler_pwm_will/data/aggKi_1.txt b/docs/routers/LSA/Regler_pwm_will/data/aggKi_1.txt new file mode 100644 index 0000000..3b04cfb --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/aggKi_1.txt @@ -0,0 +1 @@ +0.2 diff --git a/docs/routers/LSA/Regler_pwm_will/data/aggKi_2.txt b/docs/routers/LSA/Regler_pwm_will/data/aggKi_2.txt new file mode 100644 index 0000000..aec258d --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/aggKi_2.txt @@ -0,0 +1 @@ +0.8 diff --git a/docs/routers/LSA/Regler_pwm_will/data/aggKp.txt b/docs/routers/LSA/Regler_pwm_will/data/aggKp.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/aggKp.txt @@ -0,0 +1 @@ +1 diff --git a/docs/routers/LSA/Regler_pwm_will/data/aggKp_1.txt b/docs/routers/LSA/Regler_pwm_will/data/aggKp_1.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/aggKp_1.txt @@ -0,0 +1 @@ +1 diff --git a/docs/routers/LSA/Regler_pwm_will/data/aggKp_2.txt b/docs/routers/LSA/Regler_pwm_will/data/aggKp_2.txt new file mode 100644 index 0000000..a92d21c --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/aggKp_2.txt @@ -0,0 +1 @@ +.3 diff --git a/docs/routers/LSA/Regler_pwm_will/data/gp_1.txt b/docs/routers/LSA/Regler_pwm_will/data/gp_1.txt new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/gp_1.txt @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/docs/routers/LSA/Regler_pwm_will/data/gp_2.txt b/docs/routers/LSA/Regler_pwm_will/data/gp_2.txt new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/gp_2.txt @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/docs/routers/LSA/Regler_pwm_will/data/inputInt_1.txt b/docs/routers/LSA/Regler_pwm_will/data/inputInt_1.txt new file mode 100644 index 0000000..08839f6 --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/inputInt_1.txt @@ -0,0 +1 @@ +200 diff --git a/docs/routers/LSA/Regler_pwm_will/data/inputInt_2.txt b/docs/routers/LSA/Regler_pwm_will/data/inputInt_2.txt new file mode 100644 index 0000000..eb1f494 --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/inputInt_2.txt @@ -0,0 +1 @@ +500 \ No newline at end of file diff --git a/docs/routers/LSA/Regler_pwm_will/data/inputInt_3.txt b/docs/routers/LSA/Regler_pwm_will/data/inputInt_3.txt new file mode 100644 index 0000000..3d4c7bf --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/inputInt_3.txt @@ -0,0 +1 @@ +220 diff --git a/docs/routers/LSA/Regler_pwm_will/data/inputInt_4.txt b/docs/routers/LSA/Regler_pwm_will/data/inputInt_4.txt new file mode 100644 index 0000000..83be903 --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/inputInt_4.txt @@ -0,0 +1 @@ +570 \ No newline at end of file diff --git a/docs/routers/LSA/Regler_pwm_will/data/max_range_1.txt b/docs/routers/LSA/Regler_pwm_will/data/max_range_1.txt new file mode 100644 index 0000000..ace9d03 --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/max_range_1.txt @@ -0,0 +1 @@ +255 diff --git a/docs/routers/LSA/Regler_pwm_will/data/max_range_2.txt b/docs/routers/LSA/Regler_pwm_will/data/max_range_2.txt new file mode 100644 index 0000000..ace9d03 --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/max_range_2.txt @@ -0,0 +1 @@ +255 diff --git a/docs/routers/LSA/Regler_pwm_will/data/min_range_1.txt b/docs/routers/LSA/Regler_pwm_will/data/min_range_1.txt new file mode 100644 index 0000000..573541a --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/min_range_1.txt @@ -0,0 +1 @@ +0 diff --git a/docs/routers/LSA/Regler_pwm_will/data/min_range_2.txt b/docs/routers/LSA/Regler_pwm_will/data/min_range_2.txt new file mode 100644 index 0000000..573541a --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/min_range_2.txt @@ -0,0 +1 @@ +0 diff --git a/docs/routers/LSA/Regler_pwm_will/data/select_Input_SSR_1.txt b/docs/routers/LSA/Regler_pwm_will/data/select_Input_SSR_1.txt new file mode 100644 index 0000000..d00491f --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/select_Input_SSR_1.txt @@ -0,0 +1 @@ +1 diff --git a/docs/routers/LSA/Regler_pwm_will/data/select_Input_SSR_2.txt b/docs/routers/LSA/Regler_pwm_will/data/select_Input_SSR_2.txt new file mode 100644 index 0000000..573541a --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/select_Input_SSR_2.txt @@ -0,0 +1 @@ +0 diff --git a/docs/routers/LSA/Regler_pwm_will/data/test_val_1.txt b/docs/routers/LSA/Regler_pwm_will/data/test_val_1.txt new file mode 100644 index 0000000..60d3b2f --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/test_val_1.txt @@ -0,0 +1 @@ +15 diff --git a/docs/routers/LSA/Regler_pwm_will/data/test_val_2.txt b/docs/routers/LSA/Regler_pwm_will/data/test_val_2.txt new file mode 100644 index 0000000..fa8f08c --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/test_val_2.txt @@ -0,0 +1 @@ +150 diff --git a/docs/routers/LSA/Regler_pwm_will/data/www/index.html b/docs/routers/LSA/Regler_pwm_will/data/www/index.html new file mode 100644 index 0000000..2a4efb4 --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/data/www/index.html @@ -0,0 +1,153 @@ + + + + + Regler Werte Eingabe + + + + + + + + //******************************* SSR-1 ***************************************// +
+ +
+ SSR_1 Modus (current value %SSR_1_mode%): + +

+ +
+ SSR_1 Auswahl Messeingang (current value %select_Input_SSR_1%): + +

+ +
+ PWM Frequenz 1000 - 3000 Hz (current value %PWM_Freq%): + +

+
+ PWM Auflösung 4 - 16 bit (current value %PWM_Resolution%): + +

+ +
+ SSR_1 Einschalt Schwelle (current value %SSR_1_on%): + +

+ +
+ SSR_1 Ausschalt Schwelle (current value %SSR_1 off%): + +

+ +
+ Sollwert (current value %Setpoint%): + +

+
+ Distanz zum Sollwert, Regelverhalten umschalten Aggresiv - Normal (current value %SSR_1_setpoint_distance%): + +

+ + +
+ Kp (current value %Kp%): + +

+
+ Ki (current value %Ki%): + +

+
+ Kd (current value %Kd%): + +

+ +
+ aggressiv Kp (current value %aggKp%): + +

+ +
+ aggressiv Ki (current value %aggKi%): + +

+ +
+ aggressiv Kd (current value %aggKd%): + +

+ + + //******************************* SSR-2 ***************************************// +

+ +
+ SSR_2 Auswahl Messeingang (current value % select_Input_SSR_2%): + +

+ +
+ SSR_2 Modus (current value %SSR_2_mode%): + +

+ +
+ SSR_2 Einschalt Schwelle (current value %SSR_2_on%): + +

+
+ SSR_2 Ausschalt Schwelle (current value %SSR_2 off%): + +

+ + + //******************************* Einschalten Boiler, Heizung ***************************// +

+
+ Einschalten 1 (current value %activate_1% ): + +

+ + +
+ Einschalten 2 (current value %activate_2% ): + +

+ + +
+ Allgemein 1 (current value %gp_1% ): + +

+ + +
+ Allgemein 2 (current value %gp_2% ): + +

+ + //******************************* Testwerte ***************************// +

+ +
+ Testwert 1 (current value %test_val_1%): + +

+ +
+ Testwert 2 (current value %test_val_2%): + +

+ + + + + \ No newline at end of file diff --git a/docs/routers/LSA/Regler_pwm_will/littlefsbuilder.py b/docs/routers/LSA/Regler_pwm_will/littlefsbuilder.py new file mode 100644 index 0000000..93937e2 --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/littlefsbuilder.py @@ -0,0 +1,2 @@ +Import("env") +env.Replace( MKSPIFFSTOOL=env.get("PROJECT_DIR") + '/mklittlefs' ) \ No newline at end of file diff --git a/docs/routers/LSA/Regler_pwm_will/mklittlefs.exe b/docs/routers/LSA/Regler_pwm_will/mklittlefs.exe new file mode 100644 index 0000000..aa52570 Binary files /dev/null and b/docs/routers/LSA/Regler_pwm_will/mklittlefs.exe differ diff --git a/docs/routers/LSA/Regler_pwm_will/monitor/__pycache__/filter_plotter.cpython-39.pyc b/docs/routers/LSA/Regler_pwm_will/monitor/__pycache__/filter_plotter.cpython-39.pyc new file mode 100644 index 0000000..b7ab7fa Binary files /dev/null and b/docs/routers/LSA/Regler_pwm_will/monitor/__pycache__/filter_plotter.cpython-39.pyc differ diff --git a/docs/routers/LSA/Regler_pwm_will/monitor/filter_plotter.py b/docs/routers/LSA/Regler_pwm_will/monitor/filter_plotter.py new file mode 100644 index 0000000..30acc4e --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/monitor/filter_plotter.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +''' + Copyright (c) + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +''' + +import subprocess +import socket +import os +import signal +import sys +from platformio.commands.device import DeviceMonitorFilter +from platformio.project.config import ProjectConfig + +PORT = 19200 + +class SerialPlotter(DeviceMonitorFilter): + NAME = "plotter" + + def __init__(self, *args, **kwargs): + super(SerialPlotter, self).__init__(*args, **kwargs) + self.buffer = '' + self.arduplot = 'arduplot' + self.plot = None + self.plot_sock = '' + self.plot = '' + + def __call__(self): + pio_root = ProjectConfig.get_instance().get_optional_dir("core") + if sys.platform == 'win32': + self.arduplot = os.path.join(pio_root, 'penv', 'Scripts' , self.arduplot + '.cmd') + else: + self.arduplot = os.path.join(pio_root, 'penv', 'bin' , self.arduplot) + print('--- serial_plotter is starting') + self.plot = subprocess.Popen([self.arduplot, '-s', str(PORT)]) + try: + self.plot_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.plot_sock.connect(('localhost', PORT)) + except socket.error: + pass + return self + + def __del__(self): + if self.plot: + if sys.platform == 'win32': + self.plot.send_signal(signal.CTRL_C_EVENT) + self.plot.kill() + + def rx(self, text): + if self.plot.poll() is None: # None means the child is running + self.buffer += text + if '\n' in self.buffer: + try: + self.plot_sock.send(bytes(self.buffer, 'utf-8')) + except BrokenPipeError: + try: + self.plot_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.plot_sock.connect(('localhost', PORT)) + except socket.error: + pass + self.buffer = '' + else: + os.kill(os.getpid(), signal.SIGINT) + return text \ No newline at end of file diff --git a/docs/routers/LSA/Regler_pwm_will/partitions_custom.csv b/docs/routers/LSA/Regler_pwm_will/partitions_custom.csv new file mode 100644 index 0000000..97846fa --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/partitions_custom.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size, Flags +ota_0, app, ota_0, 0x10000, 0x1A0000, +ota_1, app, ota_1, , 0x1A0000, +otadata, data, ota, 0x350000, 0x2000, +nvs, data, nvs, , 0x6000, +data, data, spiffs, , 0xA8000, diff --git a/docs/routers/LSA/Regler_pwm_will/platformio.ini b/docs/routers/LSA/Regler_pwm_will/platformio.ini new file mode 100644 index 0000000..75d10f6 --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/platformio.ini @@ -0,0 +1,38 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:esp32doit-devkit-v1] + + +platform = espressif32 +board = esp32doit-devkit-v1 +build_flags = + -DASYNCWEBSERVER_REGEX +framework = arduino + + +monitor_speed = 115200 +board_build.filesystem = littlefs +lib_deps = + miq19/eModbus@^1.4.1 + knolleary/PubSubClient@^2.8 + me-no-dev/ESP Async WebServer@^1.2.3 + me-no-dev/AsyncTCP@^1.1.1 + bblanchon/ArduinoJson@^6.19.4 + + dlloydev/ESP32 ESP32S2 AnalogWrite@^2.0.9 + dlloydev/QuickPID@^3.1.2 + dlloydev/sTune@^2.4.0 + + + thomasfredericks/Bounce2@^2.71 + Wire + jandrassy/ArduinoOTA@^1.0.8 + dlloydev/sTune@^2.4.0 diff --git a/docs/routers/LSA/Regler_pwm_will/src/Regler_pwm_will.code-workspace b/docs/routers/LSA/Regler_pwm_will/src/Regler_pwm_will.code-workspace new file mode 100644 index 0000000..bab1b7f --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/src/Regler_pwm_will.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": ".." + } + ], + "settings": {} +} \ No newline at end of file diff --git a/docs/routers/LSA/Regler_pwm_will/src/credentials.h b/docs/routers/LSA/Regler_pwm_will/src/credentials.h new file mode 100644 index 0000000..c406bcc --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/src/credentials.h @@ -0,0 +1,15 @@ + + + +#define MY_SSID "Fritzbox" +#define MY_PASS "Passwort_eingeben" + + + + + + + + + + diff --git a/docs/routers/LSA/Regler_pwm_will/src/main.cpp b/docs/routers/LSA/Regler_pwm_will/src/main.cpp new file mode 100644 index 0000000..063bf15 --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/src/main.cpp @@ -0,0 +1,2104 @@ + +/* +https://github.com/lorol/LITTLEFS/issues/43 +LITTLEFSimpl::exists() bug with Platformio (ESP32 arduino framework) in Visual Studio Code +Fix by adding third argument +File f = open(path, "r", false); + +line 44 in LITTLEFS.cpp + +also found +https://github.com/espressif/arduino-esp32/pull/6179 + +With LittleFS the `fs.exists(path)` returns true also on folders. A `isDirectory()` call is required to set _isFile to false on directories. +This enables serving all files from a folder like : `server->serveStatic("/", LittleFS, "/", cacheHeader.c_str()); + File f = fs.open(path); + _isFile = (f && (! f.isDirectory())); + +line 1120 +*/ + +// Includes: for Serial etc., WiFi.h for WiFi support +#include +#include + +// for Smarty +#include +#include + +// Webserver OTA + +#include + +#include + +#include + +// #include +#include +#include "LITTLEFS.h" + +// PID +#include +#include + +// stune + +#include + +// Include the header for the ModbusClient TCP style +#include + +#include "credentials.h" + +// #define BUILTIN_LED 2 +#define Alarm 27 +#define SSR_1 26 +// #define SSR_1 13 +#define SSR_2 22 + +/////////////////// + +// #include + +#include "special_settings.h" +// switch on (1)or of (0) serial.print = serial.print replaced by debug // +// #define DEBUG 1 in special special_settings.h + +#if DEBUG == 1 +#define debug(x) Serial.print(x) +#define debugln(x) Serial.println(x) +#else +#define debug(x) +#define debugln(x) +#endif + +#if DEBUGf == 1 +#define debugf(x) Serial.printf(x) +#define debugfln(x) Serial.printfln(x) +#else +#define debugf(x) +#define debugfln(x) +#endif + +#if MQTT_DEBUG == 1 +#define mqtt_debug(x) Serial.print(x) +#define mqtt_debugln(x) Serial.println(x) +#else +#define mqtt_debug(x) +#define mqtt_debugln(x) +#endif + +#if PID_DEBUG == 1 +#define pid_debug(x) Serial.print(x) +#define pid_debugln(x) Serial.println(x) +#else +#define pid_debug(x) +#define pid_debugln(x) +#endif + +#if SSR_DEBUG == 1 +#define ssr_debug(x) Serial.print(x) +#define ssr_debugln(x) Serial.println(x) +#else +#define ssr_debug(x) +#define ssr_debugln(x) +#endif + +#if SMARTY_DEBUG == 1 +#define smarty_debug(x) Serial.print(x) +#define smarty_debugln(x) Serial.println(x) +#else +#define smarty_debug(x) +#define smarty_debugln(x) +#endif + +#if MODBUS_DEBUG == 1 +#define modbus_debug(x) Serial.print(x) +#define modbus_debugln(x) Serial.println(x) +#else +#define modbus_debug(x) +#define modbus_debugln(x) +#endif + +#if I2C_DEBUG == 1 +#define i2c -debug(x) Serial.print(x) +#define i2c_debugln(x) Serial.println(x) +#else +#define i2c_debug(x) +#define i2c_debugln(x) +#endif + +#if PWM_DEBUG == 1 +#define pwm_debug(x) Serial.print(x) +#define pwm_debugln(x) Serial.println(x) +#else +#define pwm_debug(x) +#define pwm_debugln(x) +#endif + +#if DFPLAYER_DEBUG == 1 +#define dfplayer_debug(x) Serial.print(x) +#define dfplayer_debugln(x) Serial.println(x) +#else +#define dfplayer_debug(x) +#define dfplayer_debugln(x) +#endif + +//////////////////// + +uint16_t Kostal_Pow_L1; +uint16_t Kostal_Pow_L2; +uint16_t Kostal_Pow_L3; +uint16_t Kostal_Pow_tot; + +uint16_t Kostal_Pow_L1_L2; +uint16_t Kostal_Pow_L1_L3; +uint16_t Kostal_Pow_L2_L3; + +uint16_t Basic_load_L1; +uint16_t Basic_load_L2; +uint16_t Basic_load_L3; + +uint16_t Basic_load_tot; +/* +int16_t int_Voltaik_Surplus_L1; +int16_t int_Voltaik_Surplus_L2; +int16_t int_Voltaik_Surplus_L3; +int16_t int_Voltaik_Surplus_tot; +*/ + +float float_Voltaik_Surplus_L1; +float float_Voltaik_Surplus_L2; +float float_Voltaik_Surplus_L3; +float float_Voltaik_Surplus_tot; + +uint32_t maxInflightRequests = 1; +// Für Smarty +float var_Will = 0; +float var_Bezug_tot = 0; +float var_Einsp_tot = 0; +float var_Einsp_L1 = 0; +float var_Einsp_L2 = 0; +float var_Einsp_L3 = 0; + +float val_Bezug_tot = 0; +float val_Einsp_tot = 0; +float val_Einsp_L1 = 0; +float val_Einsp_L2 = 0; +float val_Einsp_L3 = 0; + +int val_Surplus = 0; +int val_inverter = 0; + +float val_Excess_SSR_1 = 0; +float val_Excess_SSR_2 = 0; + +// Warmwasser und Heizung Temperaturen +float var_lower_temp_1 = 0; +float var_upper_temp_1 = 0; +float var_max_temp_1 = 0; + +float var_lower_temp_2 = 0; +float var_upper_temp_2 = 0; +float var_max_temp_2 = 0; + +// map range + +int min_range_1 = 10; +int max_range_1 = 20; + +int min_range_2 = 10; +int max_range_2 = 20; + +int val_SSR_1 = 0; +int val_SSR_2 = 0; + +// Solid State Relais + +float SSR_1_on; +float SSR_1_off; +float SSR_2_on; +float SSR_2_off; + +// PWM +float Var_PWM_1; + +// PID + +float Setpoint_4; +float aggKp; +float aggKi; +float aggKd; +float consKp; +float consKi; +float consKd; +float Input_4; +float Output_4; + +float Setpoint_1; +float aggKp_1; +float aggKi_1; +float aggKd_1; +float consKp_1; +float consKi_1; +float consKd_1; +float Input_1; +float Output_1; + +float Setpoint_2; +float aggKp_2; +float aggKi_2; +float aggKd_2; +float consKp_2; +float consKi_2; +float consKd_2; +float Input_2; +float Output_2; + +float Input, Output, Setpoint = 50, Kp, Ki, Kd; + +QuickPID myPID_4(&Input_4, &Output_4, &Setpoint_4); +QuickPID myPID_1(&Input_1, &Output_1, &Setpoint_1); +QuickPID myPID_2(&Input_2, &Output_2, &Setpoint_2); + +sTune tuner = sTune(&Input, &Output, tuner.ZN_PID, tuner.directIP, tuner.printOFF); + +QuickPID myPID(&Input, &Output, &Setpoint); + +// values for teting + +float test_val_1; +float test_val_2; + +// webpage Input + +const char *PARAM_INPUT_1 = "input_1"; +const char *PARAM_INPUT_2 = "input_2"; +const char *PARAM_INPUT_3 = "input_3"; +const char *PARAM_INPUT_4 = "input_4"; + +const char *PARAM_INT_1 = "inputInt_1"; +const char *PARAM_INT_2 = "inputInt_2"; +const char *PARAM_INT_3 = "inputInt_3"; +const char *PARAM_INT_4 = "inputInt_4"; + +const char *PARAM_Setpoint = "Setpoint"; +const char *PARAM_Setpoint_1 = "Setpoint_1"; +const char *PARAM_Setpoint_2 = "Setpoint_2"; + +const char *PARAM_Kp = "Kp"; +const char *PARAM_Ki = "Ki"; +const char *PARAM_Kd = "Kd"; + +const char *PARAM_aggKp = "aggKp"; +const char *PARAM_aggKi = "aggKi"; +const char *PARAM_aggKd = "aggKd"; + +const char *PARAM_Kp_1 = "Kp_1"; +const char *PARAM_Ki_1 = "Ki_1"; +const char *PARAM_Kd_1 = "Kd_1"; + +const char *PARAM_aggKp_1 = "aggKp_1"; +const char *PARAM_aggKi_1 = "aggKi_1"; +const char *PARAM_aggKd_1 = "aggKd_1"; + +const char *PARAM_Kp_2 = "Kp_2"; +const char *PARAM_Ki_2 = "Ki_2"; +const char *PARAM_Kd_2 = "Kd_2"; + +const char *PARAM_aggKp_2 = "aggKp_2"; +const char *PARAM_aggKi_2 = "aggKi_2"; +const char *PARAM_aggKd_2 = "aggKd_2"; + +const char *PARAM_SSR_1_on = "SSR_1_on"; +const char *PARAM_SSR_2_on = "SSR_2_on"; +const char *PARAM_SSR_1_off = "SSR_1_off"; +const char *PARAM_SSR_2_off = "SSR_2_off"; + +const char *PARAM_SSR_1_method = "SSR_1_method"; +const char *PARAM_SSR_2_method = "SSR_2_method"; + +const char *PARAM_SSR_1_mode = "SSR_1_mode"; +const char *PARAM_SSR_2_mode = "SSR_2_mode"; + +const char *PARAM_min_range_1 = "min_range_1"; +const char *PARAM_min_range_2 = "min_range_2"; +const char *PARAM_max_range_1 = "max_range_1"; +const char *PARAM_max_range_2 = "max_range_2"; + +const char *PARAM_select_Input_SSR_2 = "select_Input_SSR_2"; +const char *PARAM_select_Input_SSR_1 = "select_Input_SSR_1"; + +const char *PARAM_PWM_Freq = "PWM_Freq"; +const char *PARAM_PWM_Resolution = "PWM_Resolution"; + +const char *PARAM_SSR_1_setpoint_distance = "SSR_1_setpoint_distance"; +const char *PARAM_SSR_1_PID_direction = "SSR_1_PID_direction"; + +const char *PARAM_STRING = "inputString"; +const char *PARAM_INT = "inputInt"; +const char *PARAM_FLOAT = "inputFloat"; + +const char *PARAM_upper_1 = "upper_1"; +const char *PARAM_lower_1 = "lower_1"; +const char *PARAM_max_1 = "max_1"; + +const char *PARAM_upper_2 = "upper_2"; +const char *PARAM_lower_2 = "lower_2"; +const char *PARAM_max_2 = "max_2"; + +const char *PARAM_test_val_1 = "test_val_1"; +const char *PARAM_test_val_2 = "test_val_2"; +// end webpage + +// MQTT settings see credentials.h + +// Smarty subscriptions see specialsettings.h + +WiFiClient ESP32_Client; +PubSubClient MQTT_Client(ESP32_Client); +StaticJsonDocument<64> doc; + +int msg = 0; +int msg_L1 = 0; +int msg_L2 = 0; +int msg_L3 = 0; +int msg_Exp = 0; + +// Warmwater and heater subscriptions see specialsettings.h + +int msg_upper_1 = 0; +int msg_lower_1 = 0; +int msg_max_1 = 0; + +int msg_upper_2 = 0; +int msg_lower_2 = 0; +int msg_max_2 = 0; + +// LITTLEFS +unsigned int totalBytes = LITTLEFS.totalBytes(); +unsigned int usedBytes = LITTLEFS.usedBytes(); + +// Webserver for parameter data input + +AsyncWebServer httpServer(80); + +void notFound(AsyncWebServerRequest *request) +{ + request->send(404, "text/plain", "Not found"); +} + +String readFile(fs::FS &fs, const char *path) +{ + // Serial.printf("Reading file: %s\r\n", path); + File file = fs.open(path, "r"); + if (!file || file.isDirectory()) + { + debugln("- empty file or failed to open file"); + return String(); + } + // debugln("- read from file:"); + String fileContent; + while (file.available()) + { + fileContent += String((char)file.read()); + } + file.close(); + // debugln(fileContent); + return fileContent; +} + +void writeFile(fs::FS &fs, const char *path, const char *message) +{ + Serial.printf("Writing file: %s\r\n", path); + File file = fs.open(path, "w"); + if (!file) + { + debugln("- failed to open file for writing"); + return; + } + if (file.print(message)) + { + debugln("- file written"); + } + else + { + debugln("- write failed"); + } + file.close(); +} + +// Replaces placeholder with stored values + +String processor(const String &var) +{ + debugln(var); + debug("var"); + debugln(var); + if (var == "inputString") + { + return readFile(LITTLEFS, "/inputString.txt"); + } + else if (var == "inputInt") + { + return readFile(LITTLEFS, "/inputInt.txt"); + } + else if (var == "inputFloat") + { + return readFile(LITTLEFS, "/inputFloat.txt"); + } + else if (var == "inputInt_1") + { + return readFile(LITTLEFS, "/inputInt_1.txt"); + } + else if (var == "inputInt_2") + { + return readFile(LITTLEFS, "/inputInt_2.txt"); + } + else if (var == "inputInt_3") + { + return readFile(LITTLEFS, "/inputInt_3.txt"); + } + else if (var == "inputInt_4") + { + return readFile(LITTLEFS, "/inputInt_4.txt"); + } + ///////////// + + else if (var == "Setpoint") + { + return readFile(LITTLEFS, "/Setpoint.txt"); + } + + else if (var == "Setpoint_1") + { + return readFile(LITTLEFS, "/Setpoint_1.txt"); + } + else if (var == "Setpoint_2") + { + return readFile(LITTLEFS, "/Setpoint_2.txt"); + } + ///////// + else if (var == "Kp") + { + return readFile(LITTLEFS, "*/Kp.txt"); + } + else if (var == "Ki") + { + return readFile(LITTLEFS, "Ki.txt"); + } + else if (var == "Kd") + { + return readFile(LITTLEFS, "/Kd.txt"); + } + else if (var == "aggKp") + { + return readFile(LITTLEFS, "/aggKp.txt"); + } + else if (var == "aggKi") + { + return readFile(LITTLEFS, "/aggKi.txt"); + } + else if (var == "aggKd") + { + return readFile(LITTLEFS, "/aggKd.txt"); + } + + ///////// + else if (var == "Kp_1") + { + return readFile(LITTLEFS, "/Kp_1.txt"); + } + else if (var == "Ki_1") + { + return readFile(LITTLEFS, "/Ki_1.txt"); + } + else if (var == "Kd_1") + { + return readFile(LITTLEFS, "/Kd_1.txt"); + } + else if (var == "aggKp_1") + { + return readFile(LITTLEFS, "agg/Kp_1.txt"); + } + else if (var == "aggKi_1") + { + return readFile(LITTLEFS, "/aggKi_1.txt"); + } + else if (var == "aggKd_1") + { + return readFile(LITTLEFS, "/aggKd_1.txt"); + } + //////////// + else if (var == "Kp_2") + { + return readFile(LITTLEFS, "/Kp_2.txt"); + } + else if (var == "Ki_2") + { + return readFile(LITTLEFS, "/Ki_2.txt"); + } + else if (var == "Kd_2") + { + return readFile(LITTLEFS, "/Kd_2.txt"); + } + + else if (var == "aggKp_2") + { + return readFile(LITTLEFS, "/aggKp_2.txt"); + } + else if (var == "aggKi_2") + { + return readFile(LITTLEFS, "/aggKi_2.txt"); + } + else if (var == "aggKd_2") + { + return readFile(LITTLEFS, "/aggKd_2.txt"); + } + ///////////////////////////// + else if (var == "SSR_1_method") + { + return readFile(LITTLEFS, "/SSR_1_method.txt"); + } + else if (var == "SSR_2_method") + { + return readFile(LITTLEFS, "/SSR_2_method.txt"); + } + + ///////////////////////////// + else if (var == "SSR_1_mode") + { + return readFile(LITTLEFS, "/SSR_1_mode.txt"); + } + else if (var == "SSR_2_mode") + { + return readFile(LITTLEFS, "/SSR_2_mode.txt"); + } + + else if (var == "select_Input_SSR_1") + { + return readFile(LITTLEFS, "/select_Input_SSR_1.txt"); + } + else if (var == "select_Input_SSR_2") + { + return readFile(LITTLEFS, "/select_Input_SSR_2.txt"); + } + ///////////////////////////// + + ////////// + + else if (var == "min_range_1") + { + return readFile(LITTLEFS, "/min_range_1.txt"); + } + else if (var == "min_range_2") + { + return readFile(LITTLEFS, "/min_range_2.txt"); + } + else if (var == "max_range_1") + { + return readFile(LITTLEFS, "/max_range_1.txt"); + } + else if (var == "max_range_2") + { + return readFile(LITTLEFS, "/max_range_2.txt"); + } + + ///////////////////////////// + else if (var == "SSR_1_on") + { + return readFile(LITTLEFS, "/SSR_1_on.txt"); + } + else if (var == "SSR_2_on") + { + return readFile(LITTLEFS, "/SSR_2_on.txt"); + } + + ///////////////////////////// + else if (var == "SSR_1_off") + { + return readFile(LITTLEFS, "/SSR_1_off.txt"); + } + else if (var == "SSR_2_off") + { + return readFile(LITTLEFS, "/SSR_2_off.txt"); + } + + else if (var == "SSR_1_PID_direction") + { + return readFile(LITTLEFS, "/SSR_1_PID_direction.txt"); + } + else if (var == "SSR_2_PID_direction") + { + return readFile(LITTLEFS, "/SSR_2_PID_direction.txt"); + } + /////////////// + else if (var == "PWM_Freq.txt") + { + return readFile(LITTLEFS, "/PWM_Freq.txt"); + } + else if (var == "PWM_Resolution.txt") + { + return readFile(LITTLEFS, "/PWM_Resolution.txt"); + } + + else if (var == "action_1.txt") + { + return readFile(LITTLEFS, "/action_1.txt"); + } + else if (var == "action_2.txt") + { + return readFile(LITTLEFS, "/action_2.txt"); + } + else if (var == "gp_1.txt") + { + return readFile(LITTLEFS, "/gp_2.txt"); + } + else if (var == "gp_2.txt") + { + return readFile(LITTLEFS, "/gp_2.txt"); + } + + ////////////////// + else if (var == "test_val_1") + { + return readFile(LITTLEFS, "/test_val_1"); + } + else if (var == "test_val_2") + { + return readFile(LITTLEFS, "/test_val_2"); + } + + //////////////// + + return String(); +} + +// ModbusMessage DATA; + +/////////////////// + +char ssid[] = MY_SSID; // SSID and ... +char pass[] = MY_PASS; // password for the WiFi network used + +// Create a ModbusTCP client instance +ModbusClientTCPasync MB(Modbus_ip, Modbus_port); + +// Define an onData handler function to receive the regular responses +// Arguments are Modbus server ID, the function code requested, the message data and length of it, +// plus a user-supplied token to identify the causing request +void handleData(ModbusMessage response, uint32_t token) +{ + // Serial.printf("Response: serverID=%d, FC=%d, Token=%08X, length=%d:\n", response.getServerID(), response.getFunctionCode(), token, response.size()); + for (auto &byte : response) + { + // Serial.printf("%02X ", byte); + } + // debugln(""); + + switch (response.getServerID()) + { + case 1: + response.get(3, Kostal_Pow_L1); + modbus_debug("Watt Kostal L1 : "); + modbus_debugln(Kostal_Pow_L1 / 1); + break; + + case 2: + response.get(3, Kostal_Pow_L2); + modbus_debug("Watt Kostal L2 : "); + modbus_debugln(Kostal_Pow_L2 / 1); + break; + + case 3: + response.get(3, Kostal_Pow_L3); + modbus_debug("Watt Kostal L3 : "); + modbus_debugln(Kostal_Pow_L3 / 1); + break; + + case 4: + response.get(3, Kostal_Pow_tot); + modbus_debug("Watt Kostal total : "); + modbus_debugln(Kostal_Pow_tot / 1); + } + + Kostal_Pow_L1_L2 = (Kostal_Pow_L1 + Kostal_Pow_L2) / 1; + modbus_debug("Watt Kostal L1_L2 : "); + modbus_debugln(Kostal_Pow_L1_L2); + + Kostal_Pow_L2_L3 = (Kostal_Pow_L2 + Kostal_Pow_L3) / 1; + modbus_debug("Watt Kostal L2_L3 : "); + modbus_debugln(Kostal_Pow_L2_L3); + + Kostal_Pow_L1_L3 = (Kostal_Pow_L1 + Kostal_Pow_L3) / 1; + modbus_debug("Watt Kostal L1_L3 : "); + modbus_debugln(Kostal_Pow_L1_L3); +} + +// Define an onError handler function to receive error responses +// Arguments are the error code returned and a user-supplied token to identify the causing request +void handleError(Error error, uint32_t token) +{ + // ModbusError wraps the error code and provides a readable error message for it + ModbusError me(error); + Serial.printf("Error response: %02X - %s token: %d\n", (int)me, (const char *)me, token); +} + +// Setup() - initialization happens here + +void subscriptions() +{ + MQTT_Client.subscribe(Subscription_will); + MQTT_Client.subscribe(Subscription_1); + MQTT_Client.subscribe(Subscription_2); + MQTT_Client.subscribe(Subscription_3); + MQTT_Client.subscribe(Subscription_4); + MQTT_Client.subscribe(Subscription_5); + /* MQTT_Client.subscribe(Subscription_6); + MQTT_Client.subscribe(Subscription_7); + MQTT_Client.subscribe(Subscription_8); + MQTT_Client.subscribe(Subscription_9); + MQTT_Client.subscribe(Subscription_10); + MQTT_Client.subscribe(Subscription_11); + */ +} + +void mqtt_reconnect() +{ // Loop until reconnected + while (!MQTT_Client.connected()) + { + debug("Attempting MQTT connection..."); + if (MQTT_Client.connect(MQTT_CLIENT_ID)) + { // Attempt to connect + debugln("connected"); + MQTT_Client.publish(MQTT_OUT_TOPIC, "connected"); + // MQTT_Client.subscribe(MQTT_IN_TOPIC); // ... and resubscribe + + subscriptions(); + } + else + { + debugln("failed, rc=" + String(MQTT_Client.state()) + + " try again in 5 seconds"); + delay(5000); // Wait 5 seconds before retrying + } + } +} + +void mqtt_callback(char *topic, byte *payload, unsigned int length) +{ + + mqtt_debug("Message arrived in topic: "); + mqtt_debugln(topic); + + mqtt_debug("Subscribe payload:"); + for (int i = 0; i < length; i++) + { + mqtt_debug((char)payload[i]); + } + + mqtt_debugln(); + mqtt_debugln("-----------------------"); + + // Will force input to 0 + + if (strcmp(topic, Subscription_will) == 0) + { + if (msg != 1) + { + char buff_p[length]; + for (int i = 0; i < length; i++) + { + buff_p[i] = (char)payload[i]; + } + buff_p[length] = '\0'; + String msg_p = String(buff_p); + msg = 1; + + var_Will = msg_p.toFloat(); // to float + + if (var_Will == 0) + { + var_Einsp_tot = 0.0; + var_Einsp_L1 = 0.0; + var_Einsp_L2 = 0.0; + var_Einsp_L3 = 0.0; + } + } + } + + if (strcmp(topic, Subscription_1) == 0) + { + if (msg != 1) + { + char buff_p[length]; + for (int i = 0; i < length; i++) + { + buff_p[i] = (char)payload[i]; + } + buff_p[length] = '\0'; + String msg_p = String(buff_p); + msg = 1; + + var_Einsp_tot = msg_p.toFloat(); // to float + } + } + if (strcmp(topic, Subscription_2) == 0) + { + if (msg_L1 != 1) + { + char buff_p[length]; + for (int i = 0; i < length; i++) + { + buff_p[i] = (char)payload[i]; + } + buff_p[length] = '\0'; + String msg_p = String(buff_p); + var_Einsp_L1 = msg_p.toFloat(); // to float + msg_L1 = 1; + } + } + if (strcmp(topic, Subscription_3) == 0) + { + if (msg_L2 != 1) + { + char buff_p[length]; + for (int i = 0; i < length; i++) + { + buff_p[i] = (char)payload[i]; + } + buff_p[length] = '\0'; + String msg_p = String(buff_p); + var_Einsp_L2 = msg_p.toFloat(); // to float + msg_L2 = 1; + } + } + if (strcmp(topic, Subscription_4) == 0) + { + if (msg_L3 != 1) + { + char buff_p[length]; + for (int i = 0; i < length; i++) + { + buff_p[i] = (char)payload[i]; + } + buff_p[length] = '\0'; + String msg_p = String(buff_p); + var_Einsp_L3 = msg_p.toFloat(); // to float + msg_L3 = 1; + } + } + if (strcmp(topic, Subscription_5) == 0) + { + if (msg_Exp != 1) + { + char buff_p[length]; + for (int i = 0; i < length; i++) + { + buff_p[i] = (char)payload[i]; + } + buff_p[length] = '\0'; + String msg_p = String(buff_p); + msg_Exp = 1; + + var_Einsp_tot = msg_p.toFloat(); // to float + } + } + /* + if (strcmp(topic, Subscription_6) == 0) + { + if (msg_upper_1 != 1) + { + char buff_p[length]; + for (int i = 0; i < length; i++) + { + buff_p[i] = (char)payload[i]; + } + buff_p[length] = '\0'; + String msg_p = String(buff_p); + var_upper_temp_1 = msg_p.toFloat(); // to float + msg_upper_1 = 1; + } + } + if (strcmp(topic, Subscription_7) == 0) + { + if (msg_lower_1 != 1) + { + char buff_p[length]; + for (int i = 0; i < length; i++) + { + buff_p[i] = (char)payload[i]; + } + buff_p[length] = '\0'; + String msg_p = String(buff_p); + var_lower_temp_1 = msg_p.toFloat(); // to float + msg_lower_1 = 1; + } + } + if (strcmp(topic, Subscription_8) == 0) + { + if (msg_max_1 != 1) + { + char buff_p[length]; + for (int i = 0; i < length; i++) + { + buff_p[i] = (char)payload[i]; + } + buff_p[length] = '\0'; + String msg_p = String(buff_p); + var_max_temp_1 = msg_p.toFloat(); // to float + msg_max_1 = 1; + } + } + if (strcmp(topic, Subscription_9) == 0) + { + if (msg_upper_2 != 1) + { + char buff_p[length]; + for (int i = 0; i < length; i++) + { + buff_p[i] = (char)payload[i]; + } + buff_p[length] = '\0'; + String msg_p = String(buff_p); + var_upper_temp_2 = msg_p.toFloat(); // to float + msg_upper_2 = 1; + } + } + if (strcmp(topic, Subscription_10) == 0) + { + if (msg_lower_2 != 1) + { + char buff_p[length]; + for (int i = 0; i < length; i++) + { + buff_p[i] = (char)payload[i]; + } + buff_p[length] = '\0'; + String msg_p = String(buff_p); + var_lower_temp_2 = msg_p.toFloat(); // to float + msg_lower_2 = 1; + } + } + if (strcmp(topic, Subscription_11) == 0) + { + if (msg_max_2 != 1) + { + char buff_p[length]; + for (int i = 0; i < length; i++) + { + buff_p[i] = (char)payload[i]; + } + buff_p[length] = '\0'; + String msg_p = String(buff_p); + var_max_temp_2 = msg_p.toFloat(); // to float + msg_max_2 = 1; + } + } + */ +} + +void printDirectory(File dir, int numTabs = 3); + +void printDirectory(File dir, int numTabs) + +{ + while (true) + { + + File entry = dir.openNextFile(); + if (!entry) + { + // no more files + break; + } + for (uint8_t i = 0; i < numTabs; i++) + { + debug('\t'); + } + debug(entry.name()); + if (entry.isDirectory()) + { + debugln("/"); + printDirectory(entry, numTabs + 1); + } + else + { + // files have sizes, directories do not + Serial.print("\t\t"); + Serial.println(entry.size(), DEC); + } + entry.close(); + } +} + +void setup() + + // Configures static IP address + if (!WiFi.config(local_IP, gateway, subnet, primaryDNS, secondaryDNS)) { + Serial.println("STA Failed to configure"); + } + +{ + + // pinmode + + pinMode(SSR_1, OUTPUT); + pinMode(SSR_2, OUTPUT); + + /// PWM + + ledcAttachPin(SSR_1, PWM_Channel_0); + ledcSetup(PWM_Channel_0, PWM_Freq, PWM_Resolution); + + // Init Serial monitor + + Serial.begin(115200); + + while (!Serial) + { + } + debugln("__ OK __"); + + // stune + + tuner.Configure(inputSpan, outputSpan, outputStart, outputStep, testTimeSec, settleTimeSec, samples); + tuner.SetEmergencyStop(tempLimit); + + // Connect to WiFi + WiFi.begin(ssid, pass); + delay(200); + while (WiFi.status() != WL_CONNECTED) + { + debug(". "); + delay(1000); + } + + debugln(F("Inizializing FS...")); + if (LITTLEFS.begin()) + { + debugln(F("done.")); + } + else + { + debugln(F("fail.")); + } + debugln("File sistem info."); + + debug("Total space: "); + debug(totalBytes); + debugln("byte"); + + debug("Total space used: "); + debug(usedBytes); + debugln("byte"); + + debug("Total space free: "); + + debugln("byte"); + + debugln(); + + // Open dir folder + File dir = LITTLEFS.open("/"); + + // Cycle all the content + printDirectory(dir); + + // init_wifi(); + MQTT_Client.setServer(MQTT_SERVER, MQTT_PORT); + MQTT_Client.setCallback(mqtt_callback); + + //////////// + // Send web page with input fields to client + + httpServer.on("/", HTTP_GET, [](AsyncWebServerRequest *request) + { request->send(LITTLEFS, "/www/index.html", "text/html", false); }); + + httpServer.on("/", HTTP_GET, [](AsyncWebServerRequest *request) + { request->send(LITTLEFS, "/www/index.html", "text/html", processor); }); + + // Problems here with serveStatic + + // httpServer.serveStatic("/", LITTLEFS, "/").setDefaultFile("index.html");//not working see bug ??? + // httpServer.serveStatic("/index1.html", LITTLEFS, "/index1.html"); + + // Send a GET request to /get?inputString= + httpServer.on("/get", HTTP_GET, [](AsyncWebServerRequest *request) + { + String inputMessage; + // GET inputString value on /get?inputString= + + if (request->hasParam(PARAM_STRING)) { + inputMessage = request->getParam(PARAM_STRING)->value(); + writeFile(LITTLEFS, "/inputString.txt", inputMessage.c_str()); + } + + else if (request->hasParam(PARAM_INT)) { + inputMessage = request->getParam(PARAM_INT)->value(); + writeFile(LITTLEFS, "/inputInt.txt", inputMessage.c_str()); + } + + else if (request->hasParam(PARAM_FLOAT)) { + inputMessage = request->getParam(PARAM_FLOAT)->value(); + writeFile(LITTLEFS, "/inputFloat.txt", inputMessage.c_str()); + } + + else if (request->hasParam(PARAM_INT_1)) { + inputMessage = request->getParam(PARAM_INT_1)->value(); + writeFile(LITTLEFS, "/inputInt_1.txt", inputMessage.c_str()); + } + + else if (request->hasParam(PARAM_INT_2)) { + inputMessage = request->getParam(PARAM_INT_2)->value(); + writeFile(LITTLEFS, "/inputInt_2.txt", inputMessage.c_str()); + } + + else if (request->hasParam(PARAM_INT_3)) { + inputMessage = request->getParam(PARAM_INT_3)->value(); + writeFile(LITTLEFS, "/inputInt_3.txt", inputMessage.c_str()); + } + + else if (request->hasParam(PARAM_INT_4)) { + inputMessage = request->getParam(PARAM_INT_4)->value(); + writeFile(LITTLEFS, "/inputInt_4.txt", inputMessage.c_str()); + } + else if (request->hasParam(PARAM_Setpoint)) + { + inputMessage = request->getParam(PARAM_Setpoint)->value(); + writeFile(LITTLEFS, "/Setpoint.txt", inputMessage.c_str()); + } + + + else if (request->hasParam(PARAM_Setpoint_1)) { + inputMessage = request->getParam(PARAM_Setpoint_1)->value(); + writeFile(LITTLEFS, "/Setpoint_1.txt", inputMessage.c_str()); + } + + else if (request->hasParam(PARAM_Setpoint_2)) { + inputMessage = request->getParam(PARAM_Setpoint_2)->value(); + writeFile(LITTLEFS, "/Setpoint_2.txt", inputMessage.c_str()); + } + + else if (request->hasParam(PARAM_Kp)) { + inputMessage = request->getParam(PARAM_Kp)->value(); + writeFile(LITTLEFS, "/Kp.txt", inputMessage.c_str()); + } + + + else if (request->hasParam(PARAM_Ki)) { + inputMessage = request->getParam(PARAM_Ki)->value(); + writeFile(LITTLEFS, "/Ki.txt", inputMessage.c_str()); + } + + else if (request->hasParam(PARAM_Kd)) { + inputMessage = request->getParam(PARAM_Kd)->value(); + writeFile(LITTLEFS, "/Kd.txt", inputMessage.c_str()); + } + + else if(request->hasParam(PARAM_aggKp)) { + inputMessage = request->getParam(PARAM_aggKp)->value(); + writeFile(LITTLEFS, "/aggKp.txt", inputMessage.c_str()); + } + + else if(request->hasParam(PARAM_aggKi)) { + inputMessage = request->getParam(PARAM_aggKi)->value(); + writeFile(LITTLEFS, "/aggKi.txt", inputMessage.c_str()); + } + else if(request->hasParam(PARAM_aggKd)) { + inputMessage = request->getParam(PARAM_aggKd)->value(); + writeFile(LITTLEFS, "/aggKd.txt", inputMessage.c_str()); + } + + else if(request->hasParam(PARAM_Kp_1)) { + inputMessage = request->getParam(PARAM_Kp_1)->value(); + writeFile(LITTLEFS, "/Kp_1.txt", inputMessage.c_str()); + } + + + else if(request->hasParam(PARAM_Ki_1)) { + inputMessage = request->getParam(PARAM_Ki_1)->value(); + writeFile(LITTLEFS, "/Ki_1.txt", inputMessage.c_str()); + } + + else if(request->hasParam(PARAM_Kd_1)) { + inputMessage = request->getParam(PARAM_Kd_1)->value(); + writeFile(LITTLEFS, "/Kd_1.txt", inputMessage.c_str()); + } + ////////// + else if(request->hasParam(PARAM_aggKp_1)) { + inputMessage = request->getParam(PARAM_aggKp_1)->value(); + writeFile(LITTLEFS, "/aggKp_1.txt", inputMessage.c_str()); + } + + + else if(request->hasParam(PARAM_aggKi_1)) { + inputMessage = request->getParam(PARAM_aggKi_1)->value(); + writeFile(LITTLEFS, "/aggKi_1.txt", inputMessage.c_str()); + } + + else if(request->hasParam(PARAM_aggKd_1)) { + inputMessage = request->getParam(PARAM_aggKd_1)->value(); + writeFile(LITTLEFS, "/aggKd_1.txt", inputMessage.c_str()); + } + + else if(request->hasParam(PARAM_Kp_2)) { + inputMessage = request->getParam(PARAM_Kp_2)->value(); + writeFile(LITTLEFS, "/Kp_2.txt", inputMessage.c_str()); + } + + + else if(request->hasParam(PARAM_Ki_2)) { + inputMessage = request->getParam(PARAM_Ki_2)->value(); + writeFile(LITTLEFS, "/Ki_2.txt", inputMessage.c_str()); + } + + else if(request->hasParam(PARAM_Kd_2)) { + inputMessage = request->getParam(PARAM_Kd_2)->value(); + writeFile(LITTLEFS, "/Kd_2.txt", inputMessage.c_str()); + } + + + + else if(request->hasParam(PARAM_aggKp_2)) { + inputMessage = request->getParam(PARAM_aggKp_2)->value(); + writeFile(LITTLEFS, "/aggKp_2.txt", inputMessage.c_str()); + } + + + else if(request->hasParam(PARAM_aggKi_2)) { + inputMessage = request->getParam(PARAM_aggKi_2)->value(); + writeFile(LITTLEFS, "/aggKi_2.txt", inputMessage.c_str()); + } + + else if(request->hasParam(PARAM_aggKd_2)) { + inputMessage = request->getParam(PARAM_aggKd_2)->value(); + writeFile(LITTLEFS, "/aggKd_2.txt", inputMessage.c_str()); + } + + else if(request->hasParam(PARAM_select_Input_SSR_1)) { + inputMessage = request->getParam(PARAM_select_Input_SSR_1)->value(); + writeFile(LITTLEFS, "/select_Input_SSR_1.txt", inputMessage.c_str()); + } + + else if(request->hasParam(PARAM_select_Input_SSR_2)) { + inputMessage = request->getParam(PARAM_select_Input_SSR_2)->value(); + writeFile(LITTLEFS, "/select_Input_SSR_2.txt", inputMessage.c_str()); + } + + + else if(request->hasParam(PARAM_SSR_1_setpoint_distance)) { + inputMessage = request->getParam(PARAM_SSR_1_setpoint_distance)->value(); + writeFile(LITTLEFS, "/SSR_1_setpoint_distance.txt", inputMessage.c_str()); + } + + + + else if(request->hasParam(PARAM_SSR_1_PID_direction)) { + inputMessage = request->getParam(PARAM_SSR_1_PID_direction)->value(); + writeFile(LITTLEFS, "/SSR_1_PID_direction.txt", inputMessage.c_str()); + } + + + else if(request->hasParam(PARAM_SSR_1_on)) { + inputMessage = request->getParam(PARAM_SSR_1_on)->value(); + writeFile(LITTLEFS, "/SSR_1_on.txt", inputMessage.c_str()); + + } + else if(request->hasParam(PARAM_SSR_1_off)) { + inputMessage = request->getParam(PARAM_SSR_1_off)->value(); + writeFile(LITTLEFS, "/SSR_1_off.txt", inputMessage.c_str()); + } + else if(request->hasParam(PARAM_SSR_2_on)) { + inputMessage = request->getParam(PARAM_SSR_2_on)->value(); + writeFile(LITTLEFS, "/SSR_2_on.txt", inputMessage.c_str()); + + } + else if(request->hasParam(PARAM_SSR_2_off)) { + inputMessage = request->getParam(PARAM_SSR_2_off)->value(); + writeFile(LITTLEFS, "/SSR_2_off.txt", inputMessage.c_str()); + } + + + + + else if(request->hasParam(PARAM_PWM_Freq)) { + inputMessage = request->getParam(PARAM_PWM_Freq)->value(); + writeFile(LITTLEFS, "/PWM_Freq.txt", inputMessage.c_str()); + } + + else if(request->hasParam(PARAM_PWM_Resolution)) { + inputMessage = request->getParam(PARAM_PWM_Resolution)->value(); + writeFile(LITTLEFS, "/PWM_Resolution.txt", inputMessage.c_str()); + } + + + else if(request->hasParam(PARAM_SSR_1_method)) { + inputMessage = request->getParam(PARAM_SSR_1_method)->value(); + writeFile(LITTLEFS, "/SSR_1_method.txt", inputMessage.c_str()); + } + else if(request->hasParam(PARAM_SSR_2_method)) { + inputMessage = request->getParam(PARAM_SSR_2_method)->value(); + writeFile(LITTLEFS, "/SSR_2_method.txt", inputMessage.c_str()); + } + + + + else if(request->hasParam(PARAM_SSR_1_mode)) { + inputMessage = request->getParam(PARAM_SSR_1_mode)->value(); + writeFile(LITTLEFS, "/SSR_1_mode.txt", inputMessage.c_str()); + } + else if(request->hasParam(PARAM_SSR_2_mode)) { + inputMessage = request->getParam(PARAM_SSR_2_mode)->value(); + writeFile(LITTLEFS, "/SSR_2_mode.txt", inputMessage.c_str()); + } + + + else if(request->hasParam(PARAM_min_range_1)) { + inputMessage = request->getParam(PARAM_min_range_1)->value(); + writeFile(LITTLEFS, "/min_range_1.txt", inputMessage.c_str()); + } + else if(request->hasParam(PARAM_min_range_2)) { + inputMessage = request->getParam(PARAM_min_range_2)->value(); + writeFile(LITTLEFS, "/min_range_2.txt", inputMessage.c_str()); + } + else if(request->hasParam(PARAM_max_range_1)) { + inputMessage = request->getParam(PARAM_max_range_1)->value(); + writeFile(LITTLEFS, "/max_range_1.txt", inputMessage.c_str()); + } + else if(request->hasParam(PARAM_max_range_2)) { + inputMessage = request->getParam(PARAM_max_range_2)->value(); + writeFile(LITTLEFS, "/max_range_2.txt", inputMessage.c_str()); + + } + + else if(request->hasParam(PARAM_upper_1)) { + inputMessage = request->getParam(PARAM_upper_1)->value(); + writeFile(LITTLEFS, "/upper_1.txt", inputMessage.c_str()); + } + + else if(request->hasParam(PARAM_lower_1)) { + inputMessage = request->getParam(PARAM_lower_1)->value(); + writeFile(LITTLEFS, "/lower_1.txt", inputMessage.c_str()); + } + + else if(request->hasParam(PARAM_max_1)) { + inputMessage = request->getParam(PARAM_max_1)->value(); + writeFile(LITTLEFS, "/max_1.txt", inputMessage.c_str()); + } + + else if(request->hasParam(PARAM_upper_2)) { + inputMessage = request->getParam(PARAM_upper_2)->value(); + writeFile(LITTLEFS, "/upper_2.txt", inputMessage.c_str()); + } + else if(request->hasParam(PARAM_lower_2)) { + inputMessage = request->getParam(PARAM_lower_2)->value(); + writeFile(LITTLEFS, "/lower_2.txt", inputMessage.c_str()); + } + else if(request->hasParam(PARAM_max_2)) { + inputMessage = request->getParam(PARAM_max_2)->value(); + writeFile(LITTLEFS, "/max_2.txt", inputMessage.c_str()); + } + + + else if(request->hasParam(PARAM_test_val_1)) { + inputMessage = request->getParam(PARAM_test_val_1)->value(); + writeFile(LITTLEFS, "/test_val_1.txt", inputMessage.c_str()); + } + + else if(request->hasParam(PARAM_test_val_2)) { + inputMessage = request->getParam(PARAM_test_val_2)->value(); + writeFile(LITTLEFS, "/test_val_2.txt", inputMessage.c_str()); + } + else { + inputMessage = "No message sent"; + } + debugln(inputMessage); + request->send(200, "text/text", inputMessage); }); + + httpServer.onNotFound(notFound); + httpServer.begin(); + + IPAddress wIP = WiFi.localIP(); + Serial.printf("WIFi IP address: %u.%u.%u.%u\n", wIP[0], wIP[1], wIP[2], wIP[3]); + + // Set up ModbusTCP client. + // - provide onData handler function + MB.onDataHandler(&handleData); + // - provide onError handler function + MB.onErrorHandler(&handleError); + // Set message timeout to 2000ms and interval between requests to the same host to 200ms + MB.setTimeout(2000); + // Start ModbusTCP background task + MB.setIdleTimeout(10000); + MB.setMaxInflightRequests(maxInflightRequests); + + //// OTA + + // ArduinoOTA.begin(); + + // DFPlayer +} + +// End Setup + +// loop() - nothing done here today! +void loop() +{ + ArduinoOTA.handle(); // fuer Flashen ueber WLAN + if (Modbus_on_off == 1) // Modbus ein und Aus schalten + { + static unsigned long lastMillis = 0; + if (millis() - lastMillis > 5000) + { + lastMillis = millis(); + + // Create request for + // (Fill in your data here!) + // - serverID = 1 + // - function code = 0x03 (read holding register) + // - start address to read = word 40084 + // - number of words to read = 1 + // - token to match the response with the request. We take the current millis() value for it. + // + // If something is missing or wrong with the call parameters, we will immediately get an error code + // and the request will not be issued + Serial.printf("sending request with token %d\n", (uint32_t)lastMillis); + Error error; + error = MB.addRequest((uint32_t)lastMillis, Modbus_ID_1, READ_HOLD_REGISTER, Adresse_Modbus_Register_1, 1); // Abfrage 1 + error = MB.addRequest((uint32_t)lastMillis + 1, Modbus_ID_1, READ_HOLD_REGISTER, Adresse_Modbus_Register_2, 1); // Abfrage 2 + error = MB.addRequest((uint32_t)lastMillis + 2, Modbus_ID_1, READ_HOLD_REGISTER, Adresse_Modbus_Register_3, 1); // Abfrage 3 + error = MB.addRequest((uint32_t)lastMillis + 3, Modbus_ID_1, READ_HOLD_REGISTER, Adresse_Modbus_Register_4, 1); // Abfrage 4 + if (error != SUCCESS) + { + ModbusError e(error); + Serial.printf("Error creating request: %02X - %s\n", (int)e, (const char *)e); + } + + // Else the request is processed in the background task and the onData/onError handler functions will get the result. + // + // The output on the Serial Monitor will be (depending on your WiFi and Modbus the data will be different): + // __ OK __ + // . WIFi IP address: 192.168.178.74 + // Response: serverID=20, FC=3, Token=0000056C, length=11: + // 14 03 04 01 F6 FF FF FF 00 C0 A8 + } + } + if (!MQTT_Client.connected()) + { + mqtt_reconnect(); + } + + MQTT_Client.loop(); + + if (msg == 1) + { // check if new callback message + + float_Voltaik_Surplus_tot = var_Einsp_tot; // to float + // int_Voltaik_Surplus_tot = msg_t.toInt(); // to Int + mqtt_debug("Excess Solar total: "); + mqtt_debugln(float_Voltaik_Surplus_tot); + mqtt_debugln("-----------------------"); + + msg = 0; // reset message flag + + if (msg_L1 == 1) + { + float_Voltaik_Surplus_L1 = var_Einsp_L1; + mqtt_debug("Excess Solar L1: "); + mqtt_debugln(float_Voltaik_Surplus_L1); + mqtt_debugln("-----------------------"); + msg_L1 = 0; + } + + if (msg_L2 == 1) + { + float_Voltaik_Surplus_L2 = var_Einsp_L2; + mqtt_debug("Excess L2: "); + mqtt_debugln(float_Voltaik_Surplus_L2); + mqtt_debugln("-----------------------"); + msg_L2 = 0; + } + + if (msg_L3 == 1) + { + float_Voltaik_Surplus_L3 = var_Einsp_L3; + mqtt_debug("Excess L3: "); + mqtt_debugln(float_Voltaik_Surplus_L3); + mqtt_debugln("-----------------------"); + msg_L3 = 0; + } + } + + //-----------------------SSR1-------------------------// + + select_Input_SSR_1 = readFile(LITTLEFS, "/select_Input_SSR_1.txt").toFloat(); + + test_val_1 = readFile(LITTLEFS, "/test_val_1.txt").toFloat(); + test_val_2 = readFile(LITTLEFS, "/test_val_2.txt").toFloat(); + + switch (select_Input_SSR_1) + { + case 0: + ssr_debugln("input_1 case 0 "); + val_Excess_SSR_1 = 0; + ssr_debug("SSR 1 no value: "); + ssr_debugln(val_Excess_SSR_1); + ssr_debugln("-----------------------"); + break; + case 1: + ssr_debugln("input_1 case 1 "); + val_Excess_SSR_1 = float_Voltaik_Surplus_tot; + ssr_debug("SSR 1 Excess Solar total: "); + ssr_debugln(val_Excess_SSR_1); + ssr_debugln("-----------------------"); + break; + case 2: + ssr_debugln("input_1 case 2 "); + val_Excess_SSR_1 = float_Voltaik_Surplus_L1; + ssr_debug("SSR 1 Excess Solar total: "); + ssr_debugln(val_Excess_SSR_1); + ssr_debugln("-----------------------"); + break; + case 3: + ssr_debugln("input_1 case 3 "); + val_Excess_SSR_1 = float_Voltaik_Surplus_L2; + ssr_debug("SSR 1 Excess Solar total: "); + ssr_debugln(val_Excess_SSR_1); + ssr_debugln("-----------------------"); + break; + case 4: + ssr_debugln("input_1 case 4 "); + val_Excess_SSR_1 = float_Voltaik_Surplus_L3; + ssr_debug("SSR 1 Excess Solar total: "); + ssr_debugln(val_Excess_SSR_1); + ssr_debugln("-----------------------"); + break; + case 5: + ssr_debugln("input_1 case 5 "); + val_Excess_SSR_1 = Kostal_Pow_L1; + ssr_debug("SSR 1 Inverter L1: "); + ssr_debugln(val_Excess_SSR_1); + ssr_debugln("-----------------------"); + break; + case 6: + ssr_debugln("input_1 case 6 "); + val_Excess_SSR_1 = Kostal_Pow_L2; + ssr_debug("SSR 1 Inverter L2: "); + ssr_debugln(val_Excess_SSR_1); + ssr_debugln("-----------------------"); + break; + case 7: + ssr_debugln("input_1 case 7 "); + val_Excess_SSR_1 = Kostal_Pow_L3; + ssr_debug("SSR 1 Inverter L3: "); + ssr_debugln(val_Excess_SSR_1); + ssr_debugln("-----------------------"); + break; + case 8: + ssr_debugln("input_1 case 8 "); + val_Excess_SSR_1 = Kostal_Pow_tot; + ssr_debug("SSR 1 Inverter L1 L2 L3: "); + ssr_debugln(val_Excess_SSR_1); + ssr_debugln("-----------------------"); + break; + case 9: + ssr_debugln("input_1 case 9 "); + val_Excess_SSR_1 = Kostal_Pow_L1_L2; + ssr_debug("SSR 1 Inverter L1 L2 : "); + ssr_debugln(val_Excess_SSR_1); + ssr_debugln("-----------------------"); + break; + case 10: + ssr_debugln("input_1 case 10 "); + val_Excess_SSR_1 = Kostal_Pow_L1_L3; + ssr_debug("SSR 1 Inverter L1 L3: "); + ssr_debugln(val_Excess_SSR_1); + ssr_debugln("-----------------------"); + break; + case 11: + ssr_debugln("input_1 case 11 "); + val_Excess_SSR_1 = Kostal_Pow_L2_L3; + ssr_debug("SSR 1 Inverter L2 L3: "); + ssr_debugln(val_Excess_SSR_1); + ssr_debugln("-----------------------"); + break; + case 12: + ssr_debugln("input_1 case 12 "); + val_Excess_SSR_1 = test_val_1; + ssr_debug("SSR 1 test_val_1: "); + ssr_debugln(val_Excess_SSR_1); + ssr_debugln("-----------------------"); + break; + case 13: + ssr_debugln("input_1 case 13 "); + val_Excess_SSR_1 = test_val_2; + ssr_debug("SSR 1 test_val_2: "); + ssr_debugln(val_Excess_SSR_1); + ssr_debugln("-----------------------"); + break; + } + /////////////////////////////// + //-----------------------SSR2-------------------------// + + select_Input_SSR_2 = readFile(LITTLEFS, "/select_Input_SSR_2.txt").toFloat(); + test_val_1 = readFile(LITTLEFS, "/test_val_1.txt").toFloat(); + test_val_2 = readFile(LITTLEFS, "/test_val_2.txt").toFloat(); + + switch (select_Input_SSR_2) + { + case 0: + ssr_debugln("input_1 case 0 "); + val_Excess_SSR_2 = 0; + ssr_debug("SSR 2 no value: "); + ssr_debugln(val_Excess_SSR_2); + ssr_debugln("-----------------------"); + break; + case 1: + ssr_debugln("input_1 case 1 "); + val_Excess_SSR_2 = float_Voltaik_Surplus_tot; + ssr_debug("SSR 2 Excess Solar total: "); + ssr_debugln(val_Excess_SSR_2); + ssr_debugln("-----------------------"); + break; + case 2: + ssr_debugln("input_1 case 2 "); + val_Excess_SSR_2 = float_Voltaik_Surplus_L1; + ssr_debug("SSR 2 Excess Solar total: "); + ssr_debugln(val_Excess_SSR_2); + ssr_debugln("-----------------------"); + break; + case 3: + ssr_debugln("input_1 case 3 "); + val_Excess_SSR_2 = float_Voltaik_Surplus_L2; + ssr_debug("SSR 2 Excess Solar total: "); + ssr_debugln(val_Excess_SSR_2); + ssr_debugln("-----------------------"); + break; + case 4: + ssr_debugln("input_1 case 4 "); + val_Excess_SSR_2 = float_Voltaik_Surplus_L3; + ssr_debug("SSR 2 Excess Solar total: "); + ssr_debugln(val_Excess_SSR_2); + ssr_debugln("-----------------------"); + break; + case 5: + ssr_debugln("input_1 case 5 "); + val_Excess_SSR_2 = Kostal_Pow_L1; + ssr_debug("SSR 2 Inverter L1: "); + ssr_debugln(val_Excess_SSR_2); + ssr_debugln("-----------------------"); + break; + case 6: + ssr_debugln("input_1 case 6 "); + val_Excess_SSR_2 = Kostal_Pow_L2; + ssr_debug("SSR 2 Inverter L2: "); + ssr_debugln(val_Excess_SSR_2); + ssr_debugln("-----------------------"); + break; + case 7: + ssr_debugln("input_1 case 7 "); + val_Excess_SSR_2 = Kostal_Pow_L3; + ssr_debug("SSR 2 Inverter L3: "); + ssr_debugln(val_Excess_SSR_2); + ssr_debugln("-----------------------"); + break; + case 8: + ssr_debugln("input_1 case 8 "); + val_Excess_SSR_2 = Kostal_Pow_tot; + ssr_debug("SSR 2 Inverter L1 L2 L3: "); + ssr_debugln(val_Excess_SSR_2); + ssr_debugln("-----------------------"); + break; + case 9: + ssr_debugln("input_1 case 9 "); + val_Excess_SSR_2 = Kostal_Pow_L1_L2; + ssr_debug("SSR 2 Inverter L1 L2 : "); + ssr_debugln(val_Excess_SSR_2); + ssr_debugln("-----------------------"); + break; + case 10: + ssr_debugln("input_1 case 10 "); + val_Excess_SSR_2 = Kostal_Pow_L1_L3; + ssr_debug("SSR 2 Inverter L1 L3: "); + ssr_debugln(val_Excess_SSR_2); + ssr_debugln("-----------------------"); + break; + case 11: + ssr_debugln("input_1 case 11 "); + val_Excess_SSR_2 = Kostal_Pow_L2_L3; + ssr_debug("SSR 2 Inverter L2 L3: "); + ssr_debugln(val_Excess_SSR_2); + ssr_debugln("-----------------------"); + break; + case 12: + ssr_debugln("input_1 case 12 "); + val_Excess_SSR_2 = test_val_1; + + ssr_debug("SSR 2 test_val_1: "); + ssr_debugln(val_Excess_SSR_2); + ssr_debugln("-----------------------"); + break; + case 13: + ssr_debugln("input_1 case 13 "); + val_Excess_SSR_2 = test_val_2; + ssr_debug("SSR 2 test_val_2: "); + ssr_debugln(val_Excess_SSR_2); + ssr_debugln("-----------------------"); + break; + } + /////////////////////////////// + + //////////////////////////// SSR1 /////////////////////// + + SSR_1_on = readFile(LITTLEFS, "/SSR_1_on.txt").toFloat(); + SSR_1_off = readFile(LITTLEFS, "/SSR_1_off.txt").toFloat(); + SSR_1_mode = readFile(LITTLEFS, "/SSR_1_mode.txt").toFloat(); + PWM_Resolution = readFile(LITTLEFS, "/PWM_Resolution.txt").toFloat(); + + if (SSR_1_mode == 0) + { + ledcSetup(PWM_Channel_0, PWM_Freq, 12); + + ssr_debugln("------------- SSR_1 - mode 0 ---------"); + + if (val_Excess_SSR_1 < SSR_1_off) + { + Var_PWM_1 = 0; + ssr_debugln("------------- SSR_1 ----------"); + ssr_debug("SSR 1 is switched on at value :"); + ssr_debugln(SSR_1_on); + ssr_debug("SSR 1 is switched off at value : "); + ssr_debugln(SSR_1_off); + ssr_debugln("-----------------------"); + ssr_debugln("SSR 1 is off :"); + ssr_debug("value SSR_1 is : "); + ssr_debugln(val_Excess_SSR_1); + } + + if (val_Excess_SSR_1 > SSR_1_on) + { + Var_PWM_1 = 4095; + + ssr_debugln("-----------------------"); + ssr_debug("SSR 1 is switched on at value :"); + ssr_debugln(SSR_1_on); + ssr_debug("SSR 1 is switched off at value : "); + ssr_debugln(SSR_1_off); + ssr_debugln("-----------------------"); + ssr_debugln("SSR 1 is on :"); + ssr_debug("val_Excess_SSR_1 is : "); + ssr_debugln(val_Excess_SSR_1); + } + ledcWrite(PWM_Channel_0, Var_PWM_1); + } + if (SSR_1_mode == 1) + { + ledcSetup(PWM_Channel_0, PWM_Freq, 12); + + if ((val_Excess_SSR_1 >= SSR_1_off) && (val_Excess_SSR_1 <= SSR_1_on)) + { + Var_PWM_1 = map(val_Excess_SSR_1, SSR_1_off, SSR_1_on, 0, 4095); + } + if (val_Excess_SSR_1 < SSR_1_off) + { + Var_PWM_1 = 0; + } + if (val_Excess_SSR_1 > SSR_1_on) + { + Var_PWM_1 = 4095; + } + + ledcWrite(PWM_Channel_0, Var_PWM_1); + pwm_debugln("SSR 1 is switched to mapped PWM mode : "); + pwm_debug("SSR 1 PWM is : "); + pwm_debugln(Var_PWM_1); + pwm_debug("SSR 1 PWM Frequency : "); + pwm_debugln(PWM_Freq); + pwm_debug("SSR 1 PWM Resolution : "); + pwm_debugln(PWM_Resolution); + } + + //////////Mode2/////// + + if (SSR_1_mode == 2) + { + ledcSetup(PWM_Channel_0, PWM_Freq, PWM_Resolution); + // PID 2 + pid_debugln("-------------"); + pid_debugln("SSR 1 is switched to PID PWM mede: "); + SSR_1_PID_direction = readFile(LITTLEFS, "/SSR_1_PID_direction.txt").toFloat(); + PWM_Resolution = readFile(LITTLEFS, "/PWM_Resolution.txt").toFloat(); + + switch (PWM_Resolution) + { + case 4: + pid_debugln("-------------"); + pid_debugln("PID Resolution 4 bit"); + PID_max = 15; + pid_debug("PID Max Steps : 0 - "); + pid_debugln(PID_max); + break; + + case 5: + pid_debugln("-------------"); + pid_debugln("PID Resolution 5 bit"); + PID_max = 31; + pid_debug("PID Max Steps : 0 - "); + pid_debugln(PID_max); + break; + + case 6: + pid_debugln("-------------"); + pid_debugln("PID Resolution 6 bit"); + PID_max = 63; + pid_debug("PID Max Steps : 0 - "); + pid_debugln(PID_max); + break; + + case 7: + pid_debugln("-------------"); + pid_debugln("PID Resolution 7 bit"); + PID_max = 127; + pid_debug("PID Max Steps : 0 - "); + pid_debugln(PID_max); + break; + + case 8: + pid_debugln("-------------"); + pid_debugln("PID Resolution 8 bit"); + PID_max = 255; + pid_debug("PID Max Steps : 0 - "); + pid_debugln(PID_max); + break; + + case 9: + pid_debugln("-------------"); + pid_debugln("PID Resolution 9 bit"); + PID_max = 511; + pid_debug("PID Max Steps : 0 - "); + pid_debugln(PID_max); + break; + case 10: + pid_debugln("-------------"); + pid_debugln("PID Resolution 10 bit"); + PID_max = 1023; + pid_debug("PID Max Steps : 0 - "); + pid_debugln(PID_max); + break; + case 11: + pid_debugln("-------------"); + pid_debugln("PID Resolution 11 bit"); + PID_max = 2047; + pid_debug("PID Max Steps : 0 - "); + pid_debugln(PID_max); + break; + case 12: + pid_debugln("-------------"); + pid_debugln("PID Resolution 12 bit"); + PID_max = 4095; + pid_debug("PID Max Steps : 0 - "); + pid_debugln(PID_max); + break; + case 13: + pid_debugln("-------------"); + pid_debugln("PID Resolution 13 bit"); + PID_max = 8191; + pid_debug("PID Max Steps : 0 - "); + pid_debugln(PID_max); + break; + + case 14: + pid_debugln("-------------"); + pid_debugln("PID Resolution 14 bit"); + PID_max = 16383; + pid_debug("PID Max Steps : 0 - "); + pid_debugln(PID_max); + break; + + case 15: + pid_debugln("-------------"); + pid_debugln("PID Resolution 15 bit"); + PID_max = 32767; + pid_debug("PID Max Steps : 0 - "); + pid_debugln(PID_max); + break; + + case 16: + pid_debugln("-------------"); + pid_debugln("PID Resolution 16 bit"); + PID_max = 65535; + pid_debug("PID Max Steps : 0 - "); + pid_debugln(PID_max); + break; + } + + // turn the PID on + myPID_4.SetMode(myPID.Control::automatic); + myPID_4.SetOutputLimits(PID_min, PID_max); + + myPID_4.SetSampleTimeUs(SampleTimeUs); + if (SSR_1_PID_direction == 1) + { + myPID_4.SetControllerDirection(myPID_4.Action::reverse); + pid_debugln("PID direction <<---- reverse"); + } + else + { + myPID_4.SetControllerDirection(myPID_4.Action::direct); + pid_debugln("PID direction ----->> direct"); + } + + Input_4 = val_Excess_SSR_1; + Setpoint_4 = readFile(LITTLEFS, "/Setpoint.txt").toFloat(); + SSR_1_setpoint_distance = readFile(LITTLEFS, "/SSR_1_setpoint_distance.txt").toFloat(); + consKp = readFile(LITTLEFS, "/Kp.txt").toFloat(); + consKi = readFile(LITTLEFS, "/Ki.txt").toFloat(); + consKd = readFile(LITTLEFS, "/Kd.txt").toFloat(); + aggKp = readFile(LITTLEFS, "/aggKp.txt").toFloat(); + aggKi = readFile(LITTLEFS, "/aggKi.txt").toFloat(); + aggKd = readFile(LITTLEFS, "/aggKd.txt").toFloat(); + float gap_4 = abs(Setpoint_4 - Input_4); // distance away from setpoint + if (gap_4 < SSR_1_setpoint_distance) + { // we're close to setpoint, use conservative tuning parameters + myPID_4.SetTunings(consKp, consKi, consKd); + } + else + { + // we're far from setpoint, use aggressive tuning parameters + myPID_4.SetTunings(aggKp, aggKi, aggKd); + } + + myPID_4.Compute(); + + pid_debugln("-------------"); + pid_debug("Setpoint : "); + pid_debugln(Setpoint_4); + pid_debug("input: "); + pid_debugln(Input_4); + pid_debug("Setpoint distance: "); + pid_debugln(SSR_1_setpoint_distance); + pid_debug("gap: "); + pid_debugln(gap_4); + pid_debugln("-------------"); + pid_debug("SSR 1 PWM output: "); + pid_debugln(Output_4); + pid_debugln("-------------"); + pid_debug("consKp: "); + pid_debugln(consKp); + pid_debug("consKi: "); + pid_debugln(consKi); + pid_debug("consKd: "); + pid_debugln(consKd); + pid_debugln("-------------"); + pid_debug("aggKp: "); + pid_debugln(aggKp); + pid_debug("aggKi: "); + pid_debugln(aggKi); + pid_debug("aggcKd: "); + pid_debugln(aggKd); + pid_debugln("-------------"); + + ledcWrite(PWM_Channel_0, Output_4); + ssr_debugln("SSR 1 is switched to PID PWM : "); + ssr_debug("SSR 1 PWM is : "); + ssr_debugln(Output_4); + } + + if (SSR_1_mode > 2) + { + ssr_debug("SSR 1 mode incompatible out of range only 0, 1, 2: "); + } + + //////////////////////////// SSR2 /////////////////////// + SSR_2_on = readFile(LITTLEFS, "/SSR_2_on.txt").toFloat(); + SSR_2_off = readFile(LITTLEFS, "/SSR_2_off.txt").toFloat(); + SSR_2_mode = readFile(LITTLEFS, "/SSR_2_mode.txt").toFloat(); + + if (SSR_2_mode == 0) + { + if (val_Excess_SSR_2 < SSR_2_off) + { + digitalWrite(SSR_2, LOW); + ssr_debugln("-----------------------"); + ssr_debug("SSR 2 is switched on at value :"); + ssr_debugln(SSR_2_on); + ssr_debug("SSR 2 is switched off at value : "); + ssr_debugln(SSR_2_off); + ssr_debugln("-----------------------"); + ssr_debugln("SSR 2 is off :"); + ssr_debug("value SSR_2 is : "); + ssr_debugln(val_Excess_SSR_2); + } + if (val_Excess_SSR_2 > SSR_2_on) + { + digitalWrite(SSR_2, HIGH); + ssr_debugln("-----------------------"); + ssr_debug("SSR 2 is switched on at value :"); + ssr_debugln(SSR_2_on); + ssr_debug("SSR 2 is switched off at value : "); + ssr_debugln(SSR_2_off); + ssr_debugln("-----------------------"); + ssr_debugln("SSR 2 is on :"); + ssr_debug("val_Excess_SSR_2 is : "); + ssr_debugln(val_Excess_SSR_2); + } + } + if (SSR_2_mode == 1) + { + ssr_debug("SSR 2 mode PWM Mapped is not implemented : "); + } + if (SSR_2_mode == 2) + { + ssr_debug("SSR 2 mode PWM PID is not implemented : "); + } + if (SSR_2_mode > 2) + { + ssr_debug("SSR 2 mode incompatible out of range only 0, 1, 2: "); + } + + if (DEBUG == 1) + { + delay(5000); + } + else if (DEBUGf == 1) + { + delay(5000); + } + else if (MQTT_DEBUG == 1) + { + delay(5000); + } + else if (SMARTY_DEBUG == 1) + { + delay(5000); + } + else if (MODBUS_DEBUG == 1) + { + delay(5000); + } + + else if (PID_DEBUG == 1) + { + delay(5000); + } + else if (SSR_DEBUG == 1) + { + delay(5000); + } + else if (PWM_DEBUG == 1) + { + delay(5000); + } + else if (I2C_DEBUG == 1) + { + delay(5000); + } + + else + { + } +} + +// end loop \ No newline at end of file diff --git a/docs/routers/LSA/Regler_pwm_will/src/mainlittle.txt b/docs/routers/LSA/Regler_pwm_will/src/mainlittle.txt new file mode 100644 index 0000000..3988a5b --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/src/mainlittle.txt @@ -0,0 +1,924 @@ + +/* +LITTLEFSimpl::exists() bug with Platformio (ESP32 arduino framework) in Visual Studio Code +Fix by adding third argument +File f = open(path, "r", false); + +*/ + + + +// Includes: for Serial etc., WiFi.h for WiFi support +#include +#include + +// for Smarty +#include +#include + +// for modbus Invrters read power + +#include + +#include +#include +#include + +/* +#define USE_LittleFS + +#include + +#ifdef USE_LittleFS +#define SPIFFS LITTLEFS +#include +#else +#include +#endif +*/ +// PID +#include +#include +//#include + +// Include the header for the ModbusClient TCP style +#include + +#include "credentials.h" + +//#define BUILTIN_LED 2 +#define Alarm 27 +#define SSR_1 13 +#define SSR_2 14 +#define DAC1 25 // Identify the digital to analog converter pin +#define DAC2 26 // Identify the digital to analog converter pin + +/////////////////// + +//#include + +#include "special_settings.h" +// switch on (1)or of (0) serial.print = serial.print replaced by debug // +//#define DEBUG 1 in special special_settings.h + +#if DEBUG == 1 +#define debug(x) Serial.print(x) +#define debugln(x) Serial.println(x) +#else +#define debug(x) +#define debugln(x) +#endif + +//////////////////// + +uint16_t DS3_Pow_L1; +uint16_t DS3_Pow_L2; +uint16_t DS3_Pow_L3; +uint16_t DS3_Pow_tot; + +uint16_t DS3_Pow_L1_L2; +uint16_t DS3_Pow_L1_L3; +uint16_t DS3_Pow_L2_L3; + +uint16_t Basic_load_L1; +uint16_t Basic_load_L2; +uint16_t Basic_load_L3; + +uint16_t Basic_load_tot; + +uint16_t Voltaik_Surplus_L1; +uint16_t Voltaik_Surplus_L2; +uint16_t Voltaik_Surplus_L3; + +uint16_t Voltaik_Surplus_tot; + +uint32_t maxInflightRequests = 1; +// Für Smarty +float var_Bezug_tot = 0; +float var_Einsp_tot = 0; +float var_Einsp_L1 = 0; +float var_Einsp_L2 = 0; +float var_Einsp_L3 = 0; + +float val_Bezug_tot = 0; +float val_Einsp_tot = 0; +float val_Einsp_L1 = 0; +float val_Einsp_L2 = 0; +float val_Einsp_L3 = 0; + +int val_Surplus = 0; +int val_inverter = 0; + +// map range + +int min_range1 = 10; +int max_range1 = 20; + +int min_range2 = 10; +int max_range2 = 20; + +int val_DAC_1 = 0; +int val_DAC_2 = 0; +// PID + +float Setpoint_1; +float aggKp_1; +float aggKi_1; +float aggKd_1; +float consKp_1; +float consKi_1; +float consKd_1; +float Input_1; +float Output_1; + +float Setpoint_2; +float aggKp_2; +float aggKi_2; +float aggKd_2; +float consKp_2; +float consKi_2; +float consKd_2; +float Input_2; +float Output_2; + +QuickPID myPID_1(&Input_1, &Output_1, &Setpoint_1); +QuickPID myPID_2(&Input_2, &Output_2, &Setpoint_2); + +// int select_Input_1 ; +// int select_Input_2 ; + +// webpage Input + +const char* PARAM_INPUT_1 = "input_1"; +const char* PARAM_INPUT_2 = "input_2"; +const char *PARAM_INPUT_3 = "input_3"; +const char *PARAM_INPUT_4 = "input_4"; + +const char *PARAM_INT_1 = "inputInt_1"; +const char *PARAM_INT_2 = "inputInt_2"; +const char *PARAM_INT_3 = "inputInt_3"; +const char *PARAM_INT_4 = "inputInt_4"; + +const char *PARAM_Setpoint_1 = "Setpoint_1"; +const char *PARAM_Kp = "Kp"; +const char *PARAM_Ki = "Ki"; +const char *PARAM_Kd = "Kd"; + +const char *PARAM_STRING = "inputString"; +const char *PARAM_INT = "inputInt"; +const char *PARAM_FLOAT = "inputFloat"; +// Modbus on off +int MB_on_off = 1; + +// MQTT settings see credentials.h + +// Smarty subscriptions see specialsettings.h + +WiFiClient ESP32_Client; +PubSubClient MQTT_Client(ESP32_Client); +StaticJsonDocument<64> doc; + +int msg = 0; +int msg_L1 = 0; +int msg_L2 = 0; +int msg_L3 = 0; + +// LITTLEFS +unsigned int totalBytes = LITTLEFS.totalBytes(); +unsigned int usedBytes = LITTLEFS.usedBytes(); + +// Webserver for parameter data input + +AsyncWebServer server(80); + +void notFound(AsyncWebServerRequest *request) +{ + request->send(404, "text/plain", "Not found"); +} + +String readFile(fs::FS &fs, const char *path) +{ + // Serial.printf("Reading file: %s\r\n", path); + File file = fs.open(path, "r"); + if (!file || file.isDirectory()) + { + debugln("- empty file or failed to open file"); + return String(); + } + // debugln("- read from file:"); + String fileContent; + while (file.available()) + { + fileContent += String((char)file.read()); + } + file.close(); + // debugln(fileContent); + return fileContent; +} + +void writeFile(fs::FS &fs, const char *path, const char *message) +{ + Serial.printf("Writing file: %s\r\n", path); + File file = fs.open(path, "w"); + if (!file) + { + debugln("- failed to open file for writing"); + return; + } + if (file.print(message)) + { + debugln("- file written"); + } + else + { + debugln("- write failed"); + } + file.close(); +} + +// Replaces placeholder with stored values + +String processor(const String &var) +{ + debugln(var); + Serial.print("var"); + Serial.println(var); + if (var == "inputString") + { + return readFile(LITTLEFS, "/inputString.txt"); + } + else if (var == "inputInt") + { + return readFile(LITTLEFS, "/inputInt.txt"); + } + else if (var == "inputFloat") + { + return readFile(LITTLEFS, "/inputFloat.txt"); + } + else if (var == "inputInt_1") + { + return readFile(LITTLEFS, "/inputInt_1.txt"); + } + else if (var == "inputInt_2") + { + return readFile(LITTLEFS, "/inputInt_2.txt"); + } + else if (var == "inputInt_3") + { + return readFile(LITTLEFS, "/inputInt_3.txt"); + } + else if (var == "inputInt_4") + { + return readFile(LITTLEFS, "/inputInt_4.txt"); + } + else if (var == "Setpoint_1") + { + return readFile(LITTLEFS, "/Setpoint_1.txt"); + } + else if (var == "Kp") + { + return readFile(LITTLEFS, "/Kp.txt"); + } + else if (var == "Ki") + { + return readFile(LITTLEFS, "/Ki.txt"); + } + else if (var == "Kd") + { + return readFile(LITTLEFS, "/Kd.txt"); + } + return String(); +} + +// ModbusMessage DATA; + +/////////////////// + +char ssid[] = MY_SSID; // SSID and ... +char pass[] = MY_PASS; // password for the WiFi network used + +// Create a ModbusTCP client instance +ModbusClientTCPasync MB(ip, port); + +// Define an onData handler function to receive the regular responses +// Arguments are Modbus server ID, the function code requested, the message data and length of it, +// plus a user-supplied token to identify the causing request +void handleData(ModbusMessage response, uint32_t token) +{ + // Serial.printf("Response: serverID=%d, FC=%d, Token=%08X, length=%d:\n", response.getServerID(), response.getFunctionCode(), token, response.size()); + for (auto &byte : response) + { + // Serial.printf("%02X ", byte); + } + // debugln(""); + + switch (response.getServerID()) + { + case 1: + // Statement(s) + + // debugln("*****"); + response.get(3, DS3_Pow_L1); + // debug("Watt L1 : "); + // debugln(DS3_Pow_L1 / 10); + // debugln("*****"); + Voltaik_Surplus_L1 = (DS3_Pow_L1 / 10) - Basic_load_L1; + // debug("Surplus gerechnet L1 : "); + // debugln(Voltaik_Surplus_L1); + + break; + + case 2: + // debugln("*****"); + response.get(3, DS3_Pow_L2); + // debug("Watt L2 : "); + // debugln(DS3_Pow_L2 / 10); + // debugln("*****"); + Voltaik_Surplus_L2 = (DS3_Pow_L2 / 10) - Basic_load_L2; + // debug("Surplus gerechnet L2 : "); + // debugln(Voltaik_Surplus_L2); + break; + + case 3: + // debugln("*****"); + response.get(3, DS3_Pow_L3); + // debug("Watt L3 : "); + // debugln(DS3_Pow_L3 / 10); + // debugln("*****"); + Voltaik_Surplus_L3 = (DS3_Pow_L3 / 10) - Basic_load_L3; + // debug("Surplus gerechnet L3 : "); + // debugln(Voltaik_Surplus_L3); + } + + DS3_Pow_tot = (DS3_Pow_L1 + DS3_Pow_L2 + DS3_Pow_L3) / 10; + + // debugln(); + // debug("Watt_tot : "); + // debugln(DS3_Pow_tot); + // debugln(); + + DS3_Pow_L1_L2 = (DS3_Pow_L1 + DS3_Pow_L2) / 10; + // debugln(); + // debug("Watt_L1_L2 : "); + // debugln(DS3_Pow_L1_L2); + // debugln(); + + DS3_Pow_L2_L3 = (DS3_Pow_L2 + DS3_Pow_L3) / 10; + // debugln(); + // debug("Watt_L2_L3 : "); + // debugln(DS3_Pow_L2_L3); + // debugln(); + + DS3_Pow_L1_L3 = (DS3_Pow_L1 + DS3_Pow_L3) / 10; + // // debugln(); + // debug("Watt_L1_L3 : "); + // debugln(DS3_Pow_L1_L3); + // debugln(); +} + +// Define an onError handler function to receive error responses +// Arguments are the error code returned and a user-supplied token to identify the causing request +void handleError(Error error, uint32_t token) +{ + // ModbusError wraps the error code and provides a readable error message for it + ModbusError me(error); + Serial.printf("Error response: %02X - %s token: %d\n", (int)me, (const char *)me, token); +} + +// Setup() - initialization happens here + +void subscriptions() +{ + MQTT_Client.subscribe(Subscription_1); + MQTT_Client.subscribe(Subscription_2); + // MQTT_Client.subscribe(Subscription_3); + // MQTT_Client.subscribe(Subscription_4); + // MQTT_Client.subscribe(Subscription_5); +} + +void mqtt_reconnect() +{ // Loop until reconnected + while (!MQTT_Client.connected()) + { + debug("Attempting MQTT connection..."); + if (MQTT_Client.connect(MQTT_CLIENT_ID)) + { // Attempt to connect + debugln("connected"); + MQTT_Client.publish(MQTT_OUT_TOPIC, "connected"); + // MQTT_Client.subscribe(MQTT_IN_TOPIC); // ... and resubscribe + + subscriptions(); + } + else + { + debugln("failed, rc=" + String(MQTT_Client.state()) + + " try again in 5 seconds"); + delay(5000); // Wait 5 seconds before retrying + } + } +} + +void mqtt_callback(char *topic, byte *payload, unsigned int length) +{ + + debug("Message arrived in topic: "); + debugln(topic); + + debug("Subscribe JSON payload:"); + for (int i = 0; i < length; i++) + { + debug((char)payload[i]); + } + + debugln(); + debugln("-----------------------"); + + deserializeJson(doc, (byte *)payload, length); // parse MQTT message + // deserializeJson(doc,str); //can use string instead of payload + + var_Bezug_tot = doc["act_pwr_imported_p_plus_value"]; + + // debug("Bezug tot var : "); + // debugln(var_Bezug_tot)*10000; + if ((var_Bezug_tot) != 0) + { + val_Bezug_tot = var_Bezug_tot; + } + msg = 1; // message flag = 1 when new subscribe message received + + var_Einsp_tot = doc["act_pwr_exported_p_minus_value"]; + // debug("Einspeis tot var : "); + // debugln(var_Einsp_tot)*10000; + + if ((var_Einsp_tot) != 0) + { + val_Einsp_tot = var_Einsp_tot; + } + msg = 2; + /* + var_Einsp_L1 = doc["smarty-JMV/act_pwr_exp_p_minus_l1"]; + //if ((var_Einsp_L1) != 0) + //{ + + val_Einsp_L1 = var_Einsp_L1; + //} + msg_L1 = 1; + var_Einsp_L2 = doc["smarty-JMV/act_pwr_exp_p_minus_l2"]; + if ((var_Einsp_L2) != 0) + { + val_Einsp_L2 = var_Einsp_L2; + } + msg_L2 = 1; + var_Einsp_L3 = doc["smarty-JMV/act_pwr_exp_p_minus_l3"]; + if ((var_Einsp_L3) != 0) + { + val_Einsp_L3 = var_Einsp_L3; + } + msg_L3 = 1; + */ +} + +void printDirectory(File dir, int numTabs = 3); + +void printDirectory(File dir, int numTabs) +{ + while (true) + { + + File entry = dir.openNextFile(); + if (!entry) + { + // no more files + break; + } + for (uint8_t i = 0; i < numTabs; i++) + { + debug('\t'); + } + debug(entry.name()); + if (entry.isDirectory()) + { + debugln("/"); + printDirectory(entry, numTabs + 1); + } + else + { + // files have sizes, directories do not + Serial.print("\t\t"); + Serial.println(entry.size(), DEC); + } + entry.close(); + } +} + +void setup() +{ + // Init Serial monitor + Serial.begin(115200); + while (!Serial) + { + } + debugln("__ OK __"); + + // Connect to WiFi + WiFi.begin(ssid, pass); + delay(200); + while (WiFi.status() != WL_CONNECTED) + { + debug(". "); + delay(1000); + } + + // Initialize LITTLEFS + + /* if (!LITTLEFS.begin(true)) + { + debugln("An Error has occurred while mounting SPIFFS"); + return; + } + + */ + + Serial.begin(115200); + + delay(500); + + debugln(F("Inizializing FS...")); + if (LITTLEFS.begin()) + { + debugln(F("done.")); + } + else + { + debugln(F("fail.")); + } + debugln("File sistem info."); + + debug("Total space: "); + debug(totalBytes); + debugln("byte"); + + debug("Total space used: "); + debug(usedBytes); + debugln("byte"); + + Serial.print("Total space free: "); + + debugln("byte"); + + debugln(); + + // Open dir folder + File dir = LITTLEFS.open("/"); + + // Cycle all the content + printDirectory(dir); + + // init_wifi(); + MQTT_Client.setServer(MQTT_SERVER, MQTT_PORT); + MQTT_Client.setCallback(mqtt_callback); + + //////////// + // Send web page with input fields to client + + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) + { request->send(LITTLEFS, "/index.html", "text/html", false); }); + + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) + { request->send(LITTLEFS, "/index.html", "text/html", processor); }); + + // Send a GET request to /get?inputString= + server.on("/get", HTTP_GET, [](AsyncWebServerRequest *request) + { + String inputMessage; + // GET inputString value on /get?inputString= + if (request->hasParam(PARAM_STRING)) { + inputMessage = request->getParam(PARAM_STRING)->value(); + writeFile(LITTLEFS, "/inputString.txt", inputMessage.c_str()); + } + // GET inputInt value on /get?inputInt= + else if (request->hasParam(PARAM_INT)) { + inputMessage = request->getParam(PARAM_INT)->value(); + writeFile(LITTLEFS, "/inputInt.txt", inputMessage.c_str()); + } + // GET inputFloat value on /get?inputFloat= + else if (request->hasParam(PARAM_FLOAT)) { + inputMessage = request->getParam(PARAM_FLOAT)->value(); + writeFile(LITTLEFS, "/inputFloat.txt", inputMessage.c_str()); + } + // GET inputInt value on /get?inputInt_1= + else if (request->hasParam(PARAM_INT_1)) { + inputMessage = request->getParam(PARAM_INT_1)->value(); + writeFile(LITTLEFS, "/inputInt_1.txt", inputMessage.c_str()); + } + // GET inputInt value on /get?inputInt_2= + else if (request->hasParam(PARAM_INT_2)) { + inputMessage = request->getParam(PARAM_INT_2)->value(); + writeFile(LITTLEFS, "/inputInt_2.txt", inputMessage.c_str()); + } + // GET inputInt value on /get?inputInt_3= + else if (request->hasParam(PARAM_INT_3)) { + inputMessage = request->getParam(PARAM_INT_3)->value(); + writeFile(LITTLEFS, "/inputInt_3.txt", inputMessage.c_str()); + } + // GET inputInt value on /get?inputInt_4= + else if (request->hasParam(PARAM_INT_4)) { + inputMessage = request->getParam(PARAM_INT_4)->value(); + writeFile(LITTLEFS, "/inputInt_4.txt", inputMessage.c_str()); + } + // GET inputFloat value on /get?inputFloat= + else if (request->hasParam(PARAM_Setpoint_1)) { + inputMessage = request->getParam(PARAM_Setpoint_1)->value(); + writeFile(LITTLEFS, "/Setpoint_1.txt", inputMessage.c_str()); + } + // GET inputFloat value on /get?inputFloat= + else if (request->hasParam(PARAM_Kp)) { + inputMessage = request->getParam(PARAM_Kp)->value(); + writeFile(LITTLEFS, "/Kp.txt", inputMessage.c_str()); + } + + // GET inputFloat value on /get?inputFloat= + else if (request->hasParam(PARAM_Ki)) { + inputMessage = request->getParam(PARAM_Ki)->value(); + writeFile(LITTLEFS, "/Ki.txt", inputMessage.c_str()); + } + // GET inputFloat value on /get?inputFloat= + else if (request->hasParam(PARAM_Kd)) { + inputMessage = request->getParam(PARAM_Kd)->value(); + writeFile(LITTLEFS, "/Kd.txt", inputMessage.c_str()); + } + + else { + inputMessage = "No message sent"; + } + debugln(inputMessage); + request->send(200, "text/text", inputMessage); }); + server.onNotFound(notFound); + server.begin(); + + /////////////// + + IPAddress wIP = WiFi.localIP(); + Serial.printf("WIFi IP address: %u.%u.%u.%u\n", wIP[0], wIP[1], wIP[2], wIP[3]); + + // Set up ModbusTCP client. + // - provide onData handler function + MB.onDataHandler(&handleData); + // - provide onError handler function + MB.onErrorHandler(&handleError); + // Set message timeout to 2000ms and interval between requests to the same host to 200ms + MB.setTimeout(2000); + // Start ModbusTCP background task + MB.setIdleTimeout(10000); + MB.setMaxInflightRequests(maxInflightRequests); +} + +// loop() - nothing done here today! +void loop() +{ + static unsigned long lastMillis = 0; + if (millis() - lastMillis > 5000) + { + lastMillis = millis(); + + // Create request for + // (Fill in your data here!) + // - serverID = 1 + // - function code = 0x03 (read holding register) + // - start address to read = word 40084 + // - number of words to read = 1 + // - token to match the response with the request. We take the current millis() value for it. + // + // If something is missing or wrong with the call parameters, we will immediately get an error code + // and the request will not be issued + Serial.printf("sending request with token %d\n", (uint32_t)lastMillis); + Error error; + error = MB.addRequest((uint32_t)lastMillis, 1, READ_HOLD_REGISTER, 40084, 1); + error = MB.addRequest((uint32_t)lastMillis + 1, 2, READ_HOLD_REGISTER, 40084, 1); + error = MB.addRequest((uint32_t)lastMillis + 2, 3, READ_HOLD_REGISTER, 40084, 1); + if (error != SUCCESS) + { + ModbusError e(error); + Serial.printf("Error creating request: %02X - %s\n", (int)e, (const char *)e); + } + + // Else the request is processed in the background task and the onData/onError handler functions will get the result. + // + // The output on the Serial Monitor will be (depending on your WiFi and Modbus the data will be different): + // __ OK __ + // . WIFi IP address: 192.168.178.74 + // Response: serverID=20, FC=3, Token=0000056C, length=11: + // 14 03 04 01 F6 FF FF FF 00 C0 A8 + } + if (!MQTT_Client.connected()) + { + mqtt_reconnect(); + } + + MQTT_Client.loop(); + + if (msg == 2) + { // check if new callback message + + debug("Msg : "); + debugln(msg); + + val_Surplus = (val_Einsp_tot - val_Bezug_tot) * 1000; + msg = 0; // reset message flag + + debug("Bezug tot : "); + debugln(val_Bezug_tot); + + debug("Einspeis tot : "); + debugln(val_Einsp_tot); + + debug("Surplus : "); + debugln(val_Surplus); + + if (msg_L1 == 1) + { + debug("Einspeis L1 : "); + debugln(val_Einsp_L1); + debug("msg L1 : "); + debugln(msg_L1); + msg_L1 = 0; + } + if (msg_L2 == 1) + { + debug("Einspeis L2 : "); + debugln(val_Einsp_L2); + msg_L2 = 0; + } + + if (msg_L3 == 1) + { + debug("Einspeis L3 : "); + debugln(val_Einsp_L3); + msg_L3 = 0; + } + + debug("Msg : "); + debugln(msg); + } + // PID or range output. + + if (DAC_1_method == 0) + { + + // range 1 + + if (val_Surplus >= min_range1) + { + + val_DAC_1 = map(val_Surplus, min_range1, max_range1, 0, 255); + } + + if (val_Surplus < min_range1) + { + val_DAC_1 = 0; + } + } + else + { + + // PID 1 + + float gap = abs(Setpoint_1 - Input_1); // distance away from setpoint + if (gap < 10) + { // we're close to setpoint, use conservative tuning parameters + myPID_1.SetTunings(consKp_1, consKi_1, consKd_1); + } + else + { + // we're far from setpoint, use aggressive tuning parameters + myPID_1.SetTunings(aggKp_1, aggKi_1, aggKd_1); + } + myPID_1.Compute(); + val_DAC_1 = Output_1; + } + + if (DAC_2_method == 0) + { + // range 2 + + if (val_inverter >= min_range2) + { + + val_DAC_2 = map(val_Surplus, min_range2, max_range2, 0, 255); + } + if (val_Surplus < min_range2) + { + val_DAC_2 = 0; + } + } + else + { + + // PID 2 + + float gap = abs(Setpoint_2 - Input_2); // distance away from setpoint + if (gap < 10) + { // we're close to setpoint, use conservative tuning parameters + myPID_2.SetTunings(consKp_2, consKi_2, consKd_2); + } + else + { + // we're far from setpoint, use aggressive tuning parameters + myPID_1.SetTunings(aggKp_1, aggKi_1, aggKd_1); + } + myPID_1.Compute(); + val_DAC_2 = Output_2; + } + + // debug("DAC_1 Wert: "); + // debugln(val_DAC_1); + + int yourInputInt1 = readFile(LITTLEFS, "/inputInt_1.txt").toInt(); + // debug("*** DAC 1 min: "); + // debugln(yourInputInt1); + min_range1 = yourInputInt1; + // debug("min range 1: "); + // debugln(min_range1); + + int yourInputInt2 = readFile(LITTLEFS, "/inputInt_2.txt").toInt(); + // debug("*** DAC 1 max: "); + // debugln(yourInputInt2); + max_range1 = yourInputInt2; + // debug("max range 1: "); + // debugln(max_range1); + + int yourInputInt3 = readFile(LITTLEFS, "/inputInt_3.txt").toInt(); + // debug("*** DAC 2 min: "); + // debugln(yourInputInt3); + min_range2 = yourInputInt3; + // debug("min range 2: "); + // debugln(min_range2); + + int yourInputInt4 = readFile(LITTLEFS, "/inputInt_4.txt").toInt(); + // debug("*** DAC 2 max: "); + // debugln(yourInputInt4); + max_range2 = yourInputInt4; + // debug("max range 2: "); + // debugln(max_range2); + + int oldvalue_1; + if (select_Input_1 != oldvalue_1) + { + switch (select_Input_1) + { + + case 0: + debugln("input_1 case 0 "); + break; + + case 1: + debugln("input_1 case 1 "); + break; + + case 2: + debugln("input_1 case 2 "); + break; + + case 3: + debugln("input_1 case 3 "); + break; + + case 4: + debugln("input_1 case 4 "); + break; + + case 5: + debugln("input_1 case 5 "); + break; + + case 6: + debugln("input_1 case 6 "); + break; + + case 7: + debugln("input_1 case 7 "); + break; + + case 8: + debugln("input_1 case 8 "); + break; + + case 9: + debugln("input_1 case 9 "); + break; + + case 10: + debugln("input_1 case 10 "); + break; + + case 11: + debugln("input_1 case 11 "); + break; + } + + oldvalue_1 = select_Input_1; + debug("oldvalue_1 :"); + debugln(oldvalue_1); + delay(2000); + } +} // end loop \ No newline at end of file diff --git a/docs/routers/LSA/Regler_pwm_will/src/special_settings.h b/docs/routers/LSA/Regler_pwm_will/src/special_settings.h new file mode 100644 index 0000000..06e9db2 --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/src/special_settings.h @@ -0,0 +1,162 @@ +// switch on (1)or of (0) serial.print = serial.print replaced by debug // +#define DEBUG 0 +#define DEBUGf 0 +#define MQTT_DEBUG 0 +#define SMARTY_DEBUG 0 +#define MODBUS_DEBUG 0 + +#define PID_DEBUG 0 +#define SSR_DEBUG 0 +#define PWM_DEBUG 0 +#define I2C_DEBUG 0 + + +// -- Web server +// const char *http_username = "admin"; +// const char *http_password = "admin"; +// AsyncWebServer httpServer(80); +// WebSocketsServer wsServer = WebSocketsServer(81); + +// Set your Static IP address +IPAddress local_IP(192, 168, 178, 93); +// Set your Gateway IP address +IPAddress gateway(192, 168, 178, 1); + +IPAddress subnet(255, 255, 255, 0); +IPAddress primaryDNS(192, 168, 178, 1); // optional +IPAddress secondaryDNS(8, 8, 4, 4); // optional + +const char *OTA_host = "Voltaik_PWM_Control"; // Name unter welchem der virtuelle Port in der Arduino-IDE auftaucht + +// Modbus Kostal (R) +IPAddress Modbus_ip = {192, 168, 178, 97}; // IP address of modbus server +uint16_t Modbus_port = 502; // port of modbus server + +// switch on (1)or of (0) Modbus reading not yet implemented + +#define Modbus_on_off 0 // switch on (1)or of (0) + +const int Modbus_ID_1 = 1; // Kostal Modbus ID +const int Modbus_ID_2 = 2; // Modbus ID +const int Modbus_ID_3 = 3; // Modbus ID +const int Modbus_ID_4 = 4; // Modbus ID + +int Adresse_Modbus_Register_1 = 30017;//Kostal Modbus Adresse Watt L1 +int Adresse_Modbus_Register_2 = 30021;//Kostal Modbus Adresse Watt L2 +int Adresse_Modbus_Register_3 = 30025;//Kostal Modbus Adresse Watt L3 +int Adresse_Modbus_Register_4 = 30030;//Kostal Modbus Adresse Watt Total + + +// MQTT +const char *MQTT_SERVER = "192.168.178.100"; +const char *MQTT_CLIENT_ID = "Smarty_esp_Voltaik"; +const char *MQTT_OUT_TOPIC = "Voltaik_Surplus"; + +const short MQTT_PORT = 1883; // TLS=8883 + +const char *MQTT_IN_TOPIC = "#"; + +// Smarty 1 + +const char *Subscription_will = "smarty-1/lastwill/onlinestatus"; +const char *Subscription_1 = "smarty-1/power_excess_solar_calc_W"; +const char *Subscription_2 = "smarty-1/power_excess_solar_l1_calc_W"; +const char *Subscription_3 = "smarty-1/power_excess_solar_l2_calc_W"; +const char *Subscription_4 = "smarty-1/power_excess_solar_l3_calc_W"; +const char *Subscription_5 = "smarty-1/act_pwr_exported_p_minus_kW"; + + +// Boiler +//const char *Subscription_6 = "Warmwater-Mer/upper_temp"; +/* +const char *Subscription_7 = "Warmwater-Mer/lower_temp"; +const char *Subscription_8 = "Warmwater-Mer/max_temp"; +const char *Subscription_9 = "Warmwater-Mer/test_1"; +const char *Subscription_10 = "Warmwater-Mer/test_2"; +const char *Subscription_11 = "Warmwater-Mer/Test_3"; +*/ + +// SSR_1 SSR_2 +// values have to be set on webpage index.html + +// SSR_1 PWM + +int PWM_Freq = 1000; /* 1-3 KHz allowed */ +int PWM_Channel_0 = 0; +int PWM_Resolution = 12; + +// +// +int SSR_1_mode; // 0=digital 1=PWM mapped 2=Pwm PID + +// SSR_1 PID Range 0-4095 +float PID_min = 0; +float PID_max = 4095; +float SSR_1_setpoint_distance = 10; +float SSR_1_PID_direction; // 0 direct (default) or 1 reverse + +uint32_t SampleTimeUs = 15000000; + +int SSR_2_mode; // 0=digital 1=PWM mapped 2=Pwm PID attention only 0 implemented + +int select_Input_SSR_1; +/* + 0 = no input + 1 = power_excess_solar_calc_W + 2 = power_excess_solar_l1_calc_W + 3 = power_excess_solar_l2_calc_W + 4 = power_excess_solar_l3_calc_W + 5 = production Kostal Surplus L1 + 6 = production Kostal Surplus L2 + 7 = production Kostal Surplus L3 + 8 = production Kostal Surplus L1_L2_L3 + 9 = production Kostal Surplus L1_L2 + 10 = production Kostal Surplus L1_L3 + 11 = production Kostal Surplus L2_L3 + 12 = test_val_1 + 13 = test_val_2 +*/ +int select_Input_SSR_2; +/* + 0 = no input + 1 = power_excess_solar_calc_W + 2 = power_excess_solar_l1_calc_W + 3 = power_excess_solar_l2_calc_W + 4 = power_excess_solar_l3_calc_W + 5 = production Kostal Surplus L1 + 6 = production Kostal Surplus L2 + 7 = production Kostal Surplus L3 + 8 = production Kostal Surplus L1_L2_L3 + 9 = production Kostal Surplus L1_L2 + 10 = production Kostal Surplus L1_L3 + 11 = production Kostal Surplus L2_L3 + 12 = test_val_1 +13 = test_val_2 +*/ + + +int offset_DAC_2 = 2000;// need for getting positive values in reverse +/* +Controller Action + +If a positive error increases the controller’s output, the controller is said to be direct acting (i.e. heating process). +When a positive error decreases the controller’s output, the controller is said to be reverse acting (i.e. cooling process). +Since the PWM and ADC peripherals on microcontrollers only operate with positive values, QuickPID only uses positive values for Input, Output and Setpoint. +When the controller is set to REVERSE acting, the sign of the error and dInput (derivative of Input) is internally changed. All operating ranges and limits remain the same. +To simulate a REVERSE acting process from a process that’s DIRECT acting, the Input value needs to be “flipped”. +That is, if your reading from a 10-bit ADC with 0-1023 range, the input value used is (1023 - reading). See the example AutoTune_RC_Filter.ino for details. +*/ +uint32_t SampleTimeUs_2 = 15000000; + + + +//stune settimgs user settings +uint32_t settleTimeSec = 10; +uint32_t testTimeSec = 500; +const uint16_t samples = 500; +const float inputSpan = 15; +const float outputSpan = 4095; +float outputStart = 0; +float outputStep = 3; +float tempLimit = 75; + diff --git a/docs/routers/LSA/Regler_pwm_will/src/webpage1.h b/docs/routers/LSA/Regler_pwm_will/src/webpage1.h new file mode 100644 index 0000000..d371bb6 --- /dev/null +++ b/docs/routers/LSA/Regler_pwm_will/src/webpage1.h @@ -0,0 +1,650 @@ +// webpage Input + +const char *PARAM_INPUT_1 = "input_1"; +const char *PARAM_INPUT_2 = "input_2"; +const char *PARAM_INPUT_3 = "input_3"; +const char *PARAM_INPUT_4 = "input_4"; + +const char *PARAM_INT_1 = "inputInt_1"; +const char *PARAM_INT_2 = "inputInt_2"; +const char *PARAM_INT_3 = "inputInt_3"; +const char *PARAM_INT_4 = "inputInt_4"; + +const char *PARAM_Setpoint = "Setpoint"; +const char *PARAM_Setpoint_1 = "Setpoint_1"; +const char *PARAM_Setpoint_2 = "Setpoint_2"; + +const char *PARAM_Kp = "Kp"; +const char *PARAM_Ki = "Ki"; +const char *PARAM_Kd = "Kd"; + +const char *PARAM_aggKp = "aggKp"; +const char *PARAM_aggKi = "aggKi"; +const char *PARAM_aggKd = "aggKd"; + +const char *PARAM_Kp_1 = "Kp_1"; +const char *PARAM_Ki_1 = "Ki_1"; +const char *PARAM_Kd_1 = "Kd_1"; + +const char *PARAM_aggKp_1 = "aggKp_1"; +const char *PARAM_aggKi_1 = "aggKi_1"; +const char *PARAM_aggKd_1 = "aggKd_1"; + +const char *PARAM_Kp_2 = "Kp_2"; +const char *PARAM_Ki_2 = "Ki_2"; +const char *PARAM_Kd_2 = "Kd_2"; + +const char *PARAM_aggKp_2 = "aggKp_2"; +const char *PARAM_aggKi_2 = "aggKi_2"; +const char *PARAM_aggKd_2 = "aggKd_2"; + +const char *PARAM_SSR_1_on = "SSR_1_on"; +const char *PARAM_SSR_2_on = "SSR_2_on"; +const char *PARAM_SSR_1_off = "SSR_1_off"; +const char *PARAM_SSR_2_off = "SSR_2_off"; + +const char *PARAM_SSR_1_method = "SSR_1_method"; +const char *PARAM_SSR_2_method = "SSR_2_method"; + + +const char *PARAM_SSR_1_mode = "SSR_1_mode"; +const char *PARAM_SSR_2_mode = "SSR_2_mode"; + +const char *PARAM_min_range_1 = "min_range_1"; +const char *PARAM_min_range_2 = "min_range_2"; +const char *PARAM_max_range_1 = "max_range_1"; +const char *PARAM_max_range_2 = "max_range_2"; + +const char *PARAM_select_Input_SSR_2 = "select_Input_SSR_2"; +const char *PARAM_select_Input_SSR_1 = "select_Input_SSR_1"; + +const char *PARAM_PWM_Freq = "PWM_Freq"; +const char *PARAM_PWM_Resolution = "PWM_Resolution"; + + +const char *PARAM_SSR_1_setpoint_distance = "SSR_1_setpoint_distance"; +const char *PARAM_SSR_1_PID_direction = "SSR_1_PID_direction"; + +const char *PARAM_STRING = "inputString"; +const char *PARAM_INT = "inputInt"; +const char *PARAM_FLOAT = "inputFloat"; +// end webpage + +// Webserver for parameter data input + +AsyncWebServer httpServer(80); + +void notFound(AsyncWebServerRequest *request) +{ + request->send(404, "text/plain", "Not found"); +} + +String readFile(fs::FS &fs, const char *path) +{ + // Serial.printf("Reading file: %s\r\n", path); + File file = fs.open(path, "r"); + if (!file || file.isDirectory()) + { + debugln("- empty file or failed to open file"); + return String(); + } + // debugln("- read from file:"); + String fileContent; + while (file.available()) + { + fileContent += String((char)file.read()); + } + file.close(); + // debugln(fileContent); + return fileContent; +} + +void writeFile(fs::FS &fs, const char *path, const char *message +{ + Serial.printf("Writing file: %s\r\n", path); + File file = fs.open(path, "w"); + if (!file) + { + debugln("- failed to open file for writing"); + return; + } + if (file.print(message)) + { + debugln("- file written"); + } + else + { + debugln("- write failed"); + } + file.close(); +} + +// Replaces placeholder with stored values + +String processor(const String &var) +{ + debugln(var); + debug("var"); + debugln(var); + if (var == "inputString") + { + return readFile(LITTLEFS, "/inputString.txt"); + } + else if (var == "inputInt") + { + return readFile(LITTLEFS, "/inputInt.txt"); + } + else if (var == "inputFloat") + { + return readFile(LITTLEFS, "/inputFloat.txt"); + } + else if (var == "inputInt_1") + { + return readFile(LITTLEFS, "/inputInt_1.txt"); + } + else if (var == "inputInt_2") + { + return readFile(LITTLEFS, "/inputInt_2.txt"); + } + else if (var == "inputInt_3") + { + return readFile(LITTLEFS, "/inputInt_3.txt"); + } + else if (var == "inputInt_4") + { + return readFile(LITTLEFS, "/inputInt_4.txt"); + } + ///////////// + + else if (var == "Setpoint") + { + return readFile(LITTLEFS, "/Setpoint.txt"); + } + else if (var == "SSR_1_setpoint_distance") + { + return readFile(LITTLEFS, "/SSR_1_setpoint_distance.txt"); + } + + else if (var == "Setpoint_1") + { + return readFile(LITTLEFS, "/Setpoint_1.txt"); + } + else if (var == "Setpoint_2") + { + return readFile(LITTLEFS, "/Setpoint_2.txt"); + } + ///////// + else if (var == "Kp") + { + return readFile(LITTLEFS, "/Kp.txt"); + } + else if (var == "Ki") + { + return readFile(LITTLEFS, "/Ki.txt"); + } + else if (var == "Kd") + { + return readFile(LITTLEFS, "/Kd.txt"); + } + else if (var == "aggKp") + { + return readFile(LITTLEFS, "agg/Kp.txt"); + } + else if (var == "aggKi") + { + return readFile(LITTLEFS, "/aggKi.txt"); + } + else if (var == "aggKd") + { + return readFile(LITTLEFS, "/aggKd.txt"); + } + + ///////// + else if (var == "Kp_1") + { + return readFile(LITTLEFS, "/Kp_1.txt"); + } + else if (var == "Ki_1") + { + return readFile(LITTLEFS, "/Ki_1.txt"); + } + else if (var == "Kd_1") + { + return readFile(LITTLEFS, "/Kd_1.txt"); + } + else if (var == "aggKp_1") + { + return readFile(LITTLEFS, "agg/Kp_1.txt"); + } + else if (var == "aggKi_1") + { + return readFile(LITTLEFS, "/aggKi_1.txt"); + } + else if (var == "aggKd_1") + { + return readFile(LITTLEFS, "/aggKd_1.txt"); + } + //////////// + else if (var == "Kp_2") + { + return readFile(LITTLEFS, "/Kp_2.txt"); + } + else if (var == "Ki_2") + { + return readFile(LITTLEFS, "/Ki_2.txt"); + } + else if (var == "Kd_2") + { + return readFile(LITTLEFS, "/Kd_2.txt"); + } + + else if (var == "aggKp_2") + { + return readFile(LITTLEFS, "/aggKp_2.txt"); + } + else if (var == "aggKi_2") + { + return readFile(LITTLEFS, "/aggKi_2.txt"); + } + else if (var == "aggKd_2") + { + return readFile(LITTLEFS, "/aggKd_2.txt"); + } + ///////////////////////////// + else if (var == "SSR_1_method") + { + return readFile(LITTLEFS, "/SSR_1_method.txt"); + } + else if (var == "SSR_2_method") + { + return readFile(LITTLEFS, "/SSR_2_method.txt"); + } + + ///////////////////////////// + else if (var == "SSR_1_mode") + { + return readFile(LITTLEFS, "/SSR_1_mode.txt"); + } + else if (var == "SSR_2_mode") + { + return readFile(LITTLEFS, "/SSR_2_mode.txt"); + } + + else if (var == "select_Input_SSR_1") + { + return readFile(LITTLEFS, "/select_Input_SSR_1.txt"); + } + else if (var == "select_Input_SSR_2") + { + return readFile(LITTLEFS, "/select_Input_SSR_2.txt"); + } + ///////////////////////////// + + + + ////////// + + else if (var == "min_range_1") + { + return readFile(LITTLEFS, "/min_range_1.txt"); + } + else if (var == "min_range_2") + { + return readFile(LITTLEFS, "/min_range_2.txt"); + } + else if (var == "max_range_1") + { + return readFile(LITTLEFS, "/max_range_1.txt"); + } + else if (var == "max_range_2") + { + return readFile(LITTLEFS, "/max_range_2.txt"); + } + //////////////////////////////////////// + ///////////////////////////// + else if (var == "SSR_1_on") + { + return readFile(LITTLEFS, "/SSR_1_on.txt"); + } + else if (var == "SSR_2_on") + { + return readFile(LITTLEFS, "/SSR_2_on.txt"); + } + + + ///////////////////////////// + else if (var == "SSR_1_off") + { + return readFile(LITTLEFS, "/SSR_1_off.txt"); + } + else if (var == "SSR_2_off") + { + return readFile(LITTLEFS, "/SSR_2_off.txt"); + } + + else if (var == "SSR_1_PID_direction") + { + return readFile(LITTLEFS, "/SSR_1_PID_direction.txt"); + } + else if (var == "SSR_2_PID_direction") + { + return readFile(LITTLEFS, "/SSR_2_PID_direction.txt"); + } + /////////////// + else if (var == "PWM_Freq.txt") + { + return readFile(LITTLEFS, "/PWM_Freq.txt"); + } + else if (var == "PWM_Resolution.txt") + { + return readFile(LITTLEFS, "/PWM_Resolution.txt"); + } + + return String(); +} + // Send web page with input fields to client + + httpServer.on("/", HTTP_GET, [](AsyncWebServerRequest *request) + { + request->send(LITTLEFS, "/index.html", "text/html", false); }); + + httpServer.on("/", HTTP_GET, [](AsyncWebServerRequest *request) + { + request->send(LITTLEFS, "/index.html", "text/html", processor); }); + + // httpServer.serveStatic("/", LITTLEFS, "/").setDefaultFile("index.html");//not working see bug + + // Send a GET request to /get?inputString= + httpServer.on("/get", HTTP_GET, [](AsyncWebServerRequest *request) + { + String inputMessage; + // GET inputString value on /get?inputString= + if (request->hasParam(PARAM_STRING)) + { + inputMessage = request->getParam(PARAM_STRING)->value(); + writeFile(LITTLEFS, "/inputString.txt", inputMessage.c_str()); + } + // GET inputInt value on /get?inputInt= + else if (request->hasParam(PARAM_INT)) + { + inputMessage = request->getParam(PARAM_INT)->value(); + writeFile(LITTLEFS, "/inputInt.txt", inputMessage.c_str()); + } + // GET inputFloat value on /get?inputFloat= + else if (request->hasParam(PARAM_FLOAT)) + { + inputMessage = request->getParam(PARAM_FLOAT)->value(); + writeFile(LITTLEFS, "/inputFloat.txt", inputMessage.c_str()); + } + // GET inputInt value on /get?inputInt_1= + else if (request->hasParam(PARAM_INT_1)) + { + inputMessage = request->getParam(PARAM_INT_1)->value(); + writeFile(LITTLEFS, "/inputInt_1.txt", inputMessage.c_str()); + } + // GET inputInt value on /get?inputInt_2= + else if (request->hasParam(PARAM_INT_2)) + { + inputMessage = request->getParam(PARAM_INT_2)->value(); + writeFile(LITTLEFS, "/inputInt_2.txt", inputMessage.c_str()); + } + // GET inputInt value on /get?inputInt_3= + else if (request->hasParam(PARAM_INT_3)) + { + inputMessage = request->getParam(PARAM_INT_3)->value(); + writeFile(LITTLEFS, "/inputInt_3.txt", inputMessage.c_str()); + } + // GET inputInt value on /get?inputInt_4= + else if (request->hasParam(PARAM_INT_4)) + { + inputMessage = request->getParam(PARAM_INT_4)->value(); + writeFile(LITTLEFS, "/inputInt_4.txt", inputMessage.c_str()); + } + // GET inputFloat value on /get?inputFloat= + else if (request->hasParam(PARAM_Setpoint_1)) + { + inputMessage = request->getParam(PARAM_Setpoint_1)->value(); + writeFile(LITTLEFS, "/Setpoint_1.txt", inputMessage.c_str()); + } + + // GET inputFloat value on /get?inputFloat= + else if (request->hasParam(PARAM_Setpoint_2)) + { + inputMessage = request->getParam(PARAM_Setpoint_2)->value(); + writeFile(LITTLEFS, "/Setpoint_2.txt", inputMessage.c_str()); + } + // GET inputFloat value on /get?inputFloat= + else if (request->hasParam(PARAM_Kp)) + { + inputMessage = request->getParam(PARAM_Kp)->value(); + writeFile(LITTLEFS, "/Kp.txt", inputMessage.c_str()); + } + + // GET inputFloat value on /get?inputFloat= + else if (request->hasParam(PARAM_Ki)) + { + inputMessage = request->getParam(PARAM_Ki)->value(); + writeFile(LITTLEFS, "/Ki.txt", inputMessage.c_str()); + } + // GET inputFloat value on /get?inputFloat= + else if (request->hasParam(PARAM_Kd)) + { + inputMessage = request->getParam(PARAM_Kd)->value(); + writeFile(LITTLEFS, "/Kd.txt", inputMessage.c_str()); + } + ////////// + // GET inputFloat value on /get?inputFloat= + else if (request->hasParam(PARAM_Kp_1)) + { + inputMessage = request->getParam(PARAM_Kp_1)->value(); + writeFile(LITTLEFS, "/Kp_1.txt", inputMessage.c_str()); + } + + // GET inputFloat value on /get?inputFloat= + else if (request->hasParam(PARAM_Ki_1)) + { + inputMessage = request->getParam(PARAM_Ki_1)->value(); + writeFile(LITTLEFS, "/Ki_1.txt", inputMessage.c_str()); + } + // GET inputFloat value on /get?inputFloat= + else if (request->hasParam(PARAM_Kd_1)) + { + inputMessage = request->getParam(PARAM_Kd_1)->value(); + writeFile(LITTLEFS, "/Kd_1.txt", inputMessage.c_str()); + } + ////////// + else if (request->hasParam(PARAM_aggKp_1)) + { + inputMessage = request->getParam(PARAM_aggKp_1)->value(); + writeFile(LITTLEFS, "/aggKp_1.txt", inputMessage.c_str()); + } + + // GET inputFloat value on /get?inputFloat= + else if (request->hasParam(PARAM_aggKi_1)) + { + inputMessage = request->getParam(PARAM_aggKi_1)->value(); + writeFile(LITTLEFS, "/aggKi_1.txt", inputMessage.c_str()); + } + // GET inputFloat value on /get?inputFloat= + else if (request->hasParam(PARAM_aggKd_1)) + { + inputMessage = request->getParam(PARAM_aggKd_1)->value(); + writeFile(LITTLEFS, "agg/Kd_1.txt", inputMessage.c_str()); + } + ////////// + // GET inputFloat value on /get?inputFloat= + else if (request->hasParam(PARAM_Kp_2)) + { + inputMessage = request->getParam(PARAM_Kp_2)->value(); + writeFile(LITTLEFS, "/Kp_2.txt", inputMessage.c_str()); + } + + // GET inputFloat value on /get?inputFloat= + else if (request->hasParam(PARAM_Ki_2)) + { + inputMessage = request->getParam(PARAM_Ki_2)->value(); + writeFile(LITTLEFS, "/Ki_2.txt", inputMessage.c_str()); + } + // GET inputFloat value on /get?inputFloat= + else if (request->hasParam(PARAM_Kd_2)) + { + inputMessage = request->getParam(PARAM_Kd_2)->value(); + writeFile(LITTLEFS, "/Kd_2.txt", inputMessage.c_str()); + } + ////////// + // GET inputFloat value on /get?inputFloat= + else if (request->hasParam(PARAM_aggKp_2)) + { + inputMessage = request->getParam(PARAM_aggKp_2)->value(); + writeFile(LITTLEFS, "/aggKp_2.txt", inputMessage.c_str()); + } + + // GET inputFloat value on /get?inputFloat= + else if (request->hasParam(PARAM_aggKi_2)) + { + inputMessage = request->getParam(PARAM_aggKi_2)->value(); + writeFile(LITTLEFS, "/aggKi_2.txt", inputMessage.c_str()); + } + // GET inputFloat value on /get?inputFloat= + else if (request->hasParam(PARAM_aggKd_2)) + { + inputMessage = request->getParam(PARAM_aggKd_2)->value(); + writeFile(LITTLEFS, "/aggKd_2.txt", inputMessage.c_str()); + } + + else if (request->hasParam(PARAM_select_Input_SSR_1)) + { + inputMessage = request->getParam(PARAM_select_Input_SSR_1)->value(); + writeFile(LITTLEFS, "/Input_SSR_1.txt", inputMessage.c_str()); + } + + else if (request->hasParam(PARAM_select_Input_SSR_2)) + { + inputMessage = request->getParam(PARAM_select_Input_SSR_2)->value(); + writeFile(LITTLEFS, "/Input_SSR_2.txt", inputMessage.c_str()); + } + + + + else if (request->hasParam(PARAM_SSR_1_setpoint_distance)) + { + inputMessage = request->getParam(PARAM_SSR_1_setpoint_distance)->value(); + writeFile(LITTLEFS, "/SSR_1_setpoint_distance.txt", inputMessage.c_str()); + } + + else if (request->hasParam(PARAM_SSR_1_PID_direction)) + { + inputMessage = request->getParam(PARAM_SSR_1_PID_direction)->value(); + writeFile(LITTLEFS, "/SSR_1_PID_direction.txt", inputMessage.c_str()); + } + + + else if (request->hasParam(PARAM_SSR_1_on)) + { + inputMessage = request->getParam(PARAM_SSR_1_on)->value(); + writeFile(LITTLEFS, "/SSR_1_on.txt", inputMessage.c_str()); + } + else if (request->hasParam(PARAM_SSR_1_off)) + { + inputMessage = request->getParam(PARAM_SSR_1_off)->value(); + writeFile(LITTLEFS, "/SSR_1_off.txt", inputMessage.c_str()); + } + else if (request->hasParam(PARAM_SSR_2_on)) + { + inputMessage = request->getParam(PARAM_SSR_2_on)->value(); + writeFile(LITTLEFS, "/SSR_2_on.txt", inputMessage.c_str()); + } + else if (request->hasParam(PARAM_SSR_2_off)) + { + inputMessage = request->getParam(PARAM_SSR_2_off)->value(); + writeFile(LITTLEFS, "/SSR_2_off.txt", inputMessage.c_str()); + } + + + ///////////////// + + else if (request->hasParam(PARAM_PWM_Freq)) + { + inputMessage = request->getParam(PARAM_PWM_Freq)->value(); + writeFile(LITTLEFS, "/PWM_Freq.txt", inputMessage.c_str()); + } + + else if (request->hasParam(PARAM_PWM_Resolution)) + { + inputMessage = request->getParam(PARAM_PWM_Resolution)->value(); + writeFile(LITTLEFS, "/PWM_Resolution.txt", inputMessage.c_str()); + } + //////////// + + else if (request->hasParam(PARAM_SSR_1_method)) + { + inputMessage = request->getParam(PARAM_SSR_1_method)->value(); + writeFile(LITTLEFS, "/SSR_1_method.txt", inputMessage.c_str()); + } + else if (request->hasParam(PARAM_SSR_2_method)) + { + inputMessage = request->getParam(PARAM_SSR_2_method)->value(); + writeFile(LITTLEFS, "/SSR_2_method.txt", inputMessage.c_str()); + } + + //////////////////// + else if (request->hasParam(PARAM_SSR_1_mode)) + { + inputMessage = request->getParam(PARAM_SSR_1_mode)->value(); + writeFile(LITTLEFS, "/SSR_1_mode.txt", inputMessage.c_str()); + } + else if (request->hasParam(PARAM_SSR_2_mode)) + { + inputMessage = request->getParam(PARAM_SSR_2_mode)->value(); + writeFile(LITTLEFS, "/SSR_2_mode.txt", inputMessage.c_str()); + } + + + //////////////////// + + else if (request->hasParam(PARAM_min_range_1)) + { + inputMessage = request->getParam(PARAM_min_range_1)->value(); + writeFile(LITTLEFS, "/min_range_1.txt", inputMessage.c_str()); + } + else if (request->hasParam(PARAM_min_range_2)) + { + inputMessage = request->getParam(PARAM_min_range_2)->value(); + writeFile(LITTLEFS, "/min_range_2.txt", inputMessage.c_str()); + } + else if (request->hasParam(PARAM_max_range_1)) + { + inputMessage = request->getParam(PARAM_max_range_1)->value(); + writeFile(LITTLEFS, "/max_range_1.txt", inputMessage.c_str()); + } + else if (request->hasParam(PARAM_max_range_2)) + { + inputMessage = request->getParam(PARAM_max_range_2)->value(); + writeFile(LITTLEFS, "/max_range_2.txt", inputMessage.c_str()); + } + + else + { + inputMessage = "No message sent"; + } + debugln(inputMessage); + request->send(200, "text/text", inputMessage); }); + httpServer.onNotFound(notFound); + httpServer.begin(); + + /////////////// + + IPAddress wIP = WiFi.localIP(); + Serial.printf("WIFi IP address: %u.%u.%u.%u\n", wIP[0], wIP[1], wIP[2], wIP[3]); + + // Set up ModbusTCP client. + // - provide onData handler function + MB.onDataHandler(&handleData); + // - provide onError handler function + MB.onErrorHandler(&handleError); + // Set message timeout to 2000ms and interval between requests to the same host to 200ms + MB.setTimeout(2000); + // Start ModbusTCP background task + MB.setIdleTimeout(10000); + MB.setMaxInflightRequests(maxInflightRequests); +} \ No newline at end of file diff --git a/docs/routers/LSA/voltaikPID_materiel.pdf b/docs/routers/LSA/voltaikPID_materiel.pdf new file mode 100644 index 0000000..0c93712 Binary files /dev/null and b/docs/routers/LSA/voltaikPID_materiel.pdf differ diff --git "a/docs/routers/Routeur Tignous/Montage Kit Optimiseur - Forum photovolta\303\257que.pdf" "b/docs/routers/Routeur Tignous/Montage Kit Optimiseur - Forum photovolta\303\257que.pdf" new file mode 100644 index 0000000..fd68db4 Binary files /dev/null and "b/docs/routers/Routeur Tignous/Montage Kit Optimiseur - Forum photovolta\303\257que.pdf" differ diff --git a/docs/routers/Routeur Tignous/V6_cours.ino b/docs/routers/Routeur Tignous/V6_cours.ino new file mode 100755 index 0000000..7533b15 --- /dev/null +++ b/docs/routers/Routeur Tignous/V6_cours.ino @@ -0,0 +1,361 @@ +// V6 cours VERSION FRANCAISE SIMPLIFIEE UNIQUEMENT EN COMMANDE DE PHASE. du 27 / 06 /19 + +// + CORRECTION du SM à faible PUISSANCE ROUTEE . +// + DECLENCHEMENT d'une 2eme CHARGE en ZERO CROSSING suivant un seuil haut et un seuil bas +// à partir de l'info "Fd" +// version fast 1000 joules + + /* Pour dériver du surplus solaire vers un chauffe eau utilisant un relai statique non passage à zero. + * + PRINCIPE =Calculer une cinquantaine de fois par période la puissance instantannée + au noend de raccordement;moyenner sur le cycle puis en déduire le paquet d'énergie à ajouter ou soustraire + au "bucket". + A l'intérieur d'une fourchette de 100 à 1000 joules ,un triac est activé plus ou moins tard + dérivant vers une charge extérieure la quantité exacte d'énergie de façon à retorter + ou exporter un minimum. + Les échantillons doivent être décales (offset)et filtrés pour être numérisés par l'Atmel + */ + +#include +#define POSITIVE 1 +#define NEGATIVE 0 +#define ON 1 // commande positive du relai statique ou triac +#define OFF 0 +byte CdeCh1 = 2;// pin4 micro cde SSR1 +byte CdeCh2 = 4;// 8 pin 14 micro cde SSR2 (ou 4 pin 6) +byte voltageSensorPin = A3; +byte currentSensorPin = A5; +float SommeP = 0; //somme de P1 sur N periodes +int SMC =-60; // Safety Margin Compensée +float ret = 0 ; // retard + +float imaP = 0; //image de P pour calcul de SMC +long cycleCount = 0; +long cptperiodes = 0; +int samplesDuringThisMainsCycle = 0; +byte nextStateOfTriac; +float cyclesPerSecond = 50; // flottant pour plus de précision +int seuilH; //seuil enclenchemen ch2 +int seuilB; //seuil déclenchemen ch2 +int CE ; // puissance Chauffe eau +int KCE ; // coefficient puissance kCE = 8000 / CE +byte polarityNow; + +boolean flg2 = false ; // pulse cde 2 +boolean triggerNeedsToBeArmed = false; +boolean beyondStartUpPhase = false; +float Plissee = 0; +float energyInBucket = 0; +float rPWRprec ; //realPower de la période précédente +int capacityOfEnergyBucket = 1000; // AU LIEU DE 3600 +int recovPWR = 500 ;// VOIR +int sampleV,sampleI; // Les valeurs de tension et courant sont des entiers dans l'espace 0 ...1023 +int lastSampleV; // valeur à la boucle précédente. +float lastFilteredV,filteredV; // tension après filtrage pour retirer la composante continue +float prevDCoffset; // <<--- pour filtre passe bas +float DCoffset; // <<--- idem +float cumVdeltasThisCycle; // <<--- idem +float sampleVminusDC; // <<--- idem +float sampleIminusDC; // <<--- idem +float lastSampleVminusDC; // <<--- idem +float sumP; // Somme cumulée des puissances à l'intérieur d'un cycle. +// realpower est la puissance moyenne à l'intérieur d'une période + +int PHASECAL; //correction de phase inutilisé = 1 +float POWERCAL; // pour conversion des valeur brutes V et I en joules. +int VOLTAGECAL; // Pour determiner la tension mini de déclenchement du triac. // the trigger device can be safely armed + +boolean firstLoopOfHalfCycle; +boolean phaseAngleTriggerActivated; +unsigned long Tc; // = To machine à chaque début de 1/2 periode +unsigned long Fd; // firing delay + + +void setup() +{ + + wdt_enable(WDTO_8S); + + Serial.begin(500000); // pour tests + pinMode(CdeCh1, OUTPUT); + pinMode(CdeCh2, OUTPUT); + + + //++++++++ PARAMETRES A MODIFIER SUIVANT INSTALL++++++ + + CE = 2400 ; // Puissance chauffe eau + seuilH =500; //seuil enclenchemen ch2 en W approximatifs + seuilB =100; //seuil déclenchemen ch2 + +// ++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + KCE = 8000 / (CE) ; // coefficient suivant la puissance du CE pour la définition des seuils + + POWERCAL = 0.18 ; // org 0.12 à ajuster pour faire coincider la puissance vraie avec le realPwer. + // en utilisant le traceur serie. + // NON CRITIQUE car les valeur absolues s'annulent en phase de régulation // retort and export flows are balanced. + + VOLTAGECAL = (float)679 / 471; // En volts par pas d'ADC. + // Utilisé pour déterminer quand la tension secteur est suffisante pour + // exciter le triac. noter les valeurs min et max de la mesure de tension + // par exemple 678.8 pic pic + // La dynamique étant de 471 signifie une sous estimation de la tension + // de 471/679. VOLTAGECAL doit donc être multiplié par l'inverse + // de 679/471 soit 1.44 + PHASECAL = 1; // NON CRITIQUE + + sumP = 0 ; +} + + +void loop() // Une paire tension / courant est mesurée à chaque boucle (environ 54 par période) + + { + wdt_reset(); + samplesDuringThisMainsCycle++; // incrément du nombre d'échantillons par période secteur pour calcul de puissance. + + // Sauvegarde des échantillons précédents + lastSampleV=sampleV; // pour le filtre passe haut + lastFilteredV = filteredV; // afin d'identifier le début de chaque période + lastSampleVminusDC = sampleVminusDC; // for phasecal calculation + +// Acquisition d'une nouvelle paire d'chantillons bruts. temps total :380µS + + sampleI = analogRead(currentSensorPin); + sampleV = analogRead(voltageSensorPin); + + // Soustraction de la composante continue déterminée par le filtre passe bas + sampleVminusDC = sampleV - DCoffset; + sampleIminusDC = sampleI - DCoffset; + + // Un filtre passe haut est utilisé pour déterminer le début de cycle. + filteredV = 0.996*(lastFilteredV+sampleV-lastSampleV); // Sinus tension reconstituée + // lastFilteredV = zéro en début de cycle + + digitalWrite(CdeCh1, OFF); // + + // Détection de la polarité de l'alternance + byte polarityOfLastReading = polarityNow; + + if(filteredV >= 0) + polarityNow = POSITIVE; + else + polarityNow = NEGATIVE; + + if (polarityNow == POSITIVE) + { + if (polarityOfLastReading != POSITIVE) + { + // C'est le départ d'une nouvelle sinus positive juste après le passage à zéro + + cycleCount++; // incrément Nb de périodes + cptperiodes++; // pour affichage toutes les 50 périodes de Power + firstLoopOfHalfCycle = true; + + // mise à jour du filtre passe bas pour la soustraction de la composante continue + prevDCoffset = DCoffset; + DCoffset = prevDCoffset + (0.015 * cumVdeltasThisCycle); + + // Calcul la puissance réelle de toutes les mesures echantillonnées durant + // le cycle précedent, et determination du gain (ou la perte) en energie. + float realPower = POWERCAL * sumP / (float)samplesDuringThisMainsCycle; + float realEnergy = realPower / cyclesPerSecond;//Pmoy X 0.02 en joules sur une période + + + //**********cas de variation brusque et importante ****** + // pour eviter un pic d'injection , on repart à puissance moindre + float deltaP = realPower - rPWRprec ; // puissance période - puissance période précédente + rPWRprec = realPower ; // mise à jour + if ( deltaP > recovPWR) // valeur à ajuster + energyInBucket = 300 ; // routage sur Pmax /3 + + + if (beyondStartUpPhase == true)// > 2 secondes + { + // Supposant que les filtres ont eu suffisamment de temps de se stabiliser + // ajout de cette energie d'une période à l'énergie du reservoir. + energyInBucket += realEnergy; + + // Reduction d'énergie dans le reservoir d'une quantité "safety margin" + // Ceci permet au système un décalage pour plus d'injection ou + de soutirage + energyInBucket -= SMC / cyclesPerSecond; + + // Limites dans la fourchette 0 ...1000 joules ;ne peut être négatif le 11 / 2 / 18 + if (energyInBucket > capacityOfEnergyBucket) + energyInBucket = capacityOfEnergyBucket; + if (energyInBucket < 0) + energyInBucket = 0; + } + else + { + // Après un reset attendre 100 périodes (2 secondes) le temps que la composante continue soit éliminée par le filtre + if(cycleCount > 100) // deux secondes + beyondStartUpPhase = true; //croisière + } + triggerNeedsToBeArmed = true; // déclenchement armé à chaque cycle + + // ******************************************************** + + // determination du retard de déclenchement du triac + + // Ne pas allumer si l'énergie du bucket est trop basse (Fd = Firing Delay) + if (energyInBucket <= 100) + {Fd = 99999; + } + else + + if (energyInBucket >= 1000) + { Fd = 200;// déclencher immediatement si le niveau d'energie est au dessus du max + + } + + // determination du bon point de déclenchement pour un niveau donné + // algorithme simple + // Fd est le retard au déclenchement en microsecondes du début de la sinus . + // pour Pmin = 10000 corrigé à 8500 + // pour Pmax = 0 corrigé à 200 + + else + + { + Fd = 10 * (1020 - energyInBucket); + + ret = (Fd); + if (ret >= 8000) + ret = 8000; // LIMITE BASSE 8000 soit 50W + imaP = 8000 - (ret) ; + + SMC = -60 ; //Base de -60 + + if ((Fd > 7300)) // <200w compensation pour moindre soutirage à basse puissance + SMC = -30 ; // -30 par défaut + + + if (Fd > 8500) // pas de déclenchement + { Fd = 99999; + + } + } + //****************************************************** + // imaP des SEUILS imaP est une image de la puissance routée (8000-(Fd))/N environ 1500 W max + + // SommeP tient compte de la puissance du Chauffe eau + // Plissee est SommeP moyennée sur N périodes + + (SommeP += (imaP / (KCE))) ; // Puissance routée par période secteur et incrémentée + + if (cptperiodes==250) //Pmoy lissée ;par exemple sur 250 secondes + + { Plissee = SommeP / 250 ; //moyenne sur N échantillons } + if (Plissee >= seuilH) //seuil enclenchemen ch2 + { flg2 = true ; + } + if (Plissee <= seuilB) + { flg2 = false ; + + } // sinon ,on coupe + + // Réinitialisation avant un nouveau moyennage + + cptperiodes = 0; + SommeP = 0; + + // POUR tests + // Serial.print((Plissee), 0); + // Serial.print ("\t\t"); + // Serial.print((KCE), 0) ; + // Serial.print ("\t\t"); + // Serial.print ("\t\t"); + // Serial.println((imaP), 0) ; + + } + sumP = 0;// somme des puissances instantannées + samplesDuringThisMainsCycle = 0; + cumVdeltasThisCycle = 0;// somme des tensions instantannées filtrées + } // Fin du processus spécifique au premier échantillon +ve d'un nouveau cycle secteur + + // suite du traitement des échantillons de tension POSITIFS ... + + } // Fin du processus sur la demi alternance positive (tension) + else + { + if (polarityOfLastReading != NEGATIVE) + { + firstLoopOfHalfCycle = true; + } + } + // Processus pour TOUS les échantillons, positifs et negatifs 54 fois par période + + + unsigned long To = micros(); // Nb de microsSec depuis le lancement du PG + + if (flg2 == true ) + { {digitalWrite(CdeCh2, ON) ;}} + else + { {digitalWrite(CdeCh2, OFF) ;}} + +if (firstLoopOfHalfCycle == true) + + { Tc = To; // mise à l'heure en début de 1/2 alternance + firstLoopOfHalfCycle = false; + phaseAngleTriggerActivated = false; + // Autre que P max,annuler le déclenchement a la première boucle + // de chaque demi cycle pour être sur qu'il ne reste pas bloqué ON. + if(Fd > 200) + + { digitalWrite(CdeCh1, OFF);} + } + if (phaseAngleTriggerActivated == true) + { + // Sauf demande de puissance max,désarmer le déclenchement a chaque boucle + // après la conduction de la demi période. durée du pulse 20000 / 54 = 370µS + + if (Fd > 200) + { digitalWrite(CdeCh1, OFF); } + } + else + { + if (To >= (Tc + Fd)) + { digitalWrite(CdeCh1, ON); // at To + Firing delay + phaseAngleTriggerActivated = true; } + + } + // Fin de la gestion de l'allumage du Triac + //******************************************************* + + + // Apply phase-shift to the voltage waveform to ensure that the system measures a + // resistive load with a power factor of unity. + float phaseShiftedVminusDC = + lastSampleVminusDC + PHASECAL * (sampleVminusDC - lastSampleVminusDC); + float instP = phaseShiftedVminusDC * sampleIminusDC; // PUISSANCE derniers échantillons V x I filtrés + sumP +=instP; // accumulation à chaque boucle des puissances instantanées dans une période + + cumVdeltasThisCycle += (sampleV - DCoffset); // pour usage avec filtre passe bas + + +} // end of loop() + +/*Fd Puissance sur charge 1500 W + 8,5 20 + 8 50 + 7,5 100 + 7 200 + 6.5 300 + 6 430 + 5,5 550 + 5 750 + 4,5 926 + 4 1015 + 3,5 1100 + 3 1235 + 2,5 1340 + 2 1455 + 0 1500 + // simple algorithm (with non-linear power response across the energy range) +// firingDelayInMicros = 10 * (2300 - energyInBucket); + + // complex algorithm which reflects the non-linear nature of phase-angle control. + firingDelayInMicros = (asin((-1 * (energyInBucket - 1800) / 500)) + (PI/2)) * (10000/PI); + +*/ diff --git a/docs/routers/Routeur Tignous/V7_cours.ino b/docs/routers/Routeur Tignous/V7_cours.ino new file mode 100755 index 0000000..03c0edf --- /dev/null +++ b/docs/routers/Routeur Tignous/V7_cours.ino @@ -0,0 +1,282 @@ +// V7 VERSION FRANCAISE SIMPLIFIEE UNIQUEMENT EN COMMANDE DE PHASE. +// Rv modifications mars 2021 pour supprimer le soutirage résiduel qui n'a pas d'interet lorsque l'on est pas en CACSI + +// + DECLENCHEMENT d'une 2eme CHARGE en ZERO CROSSING suivant un seuil haut et un seuil bas +// à partir de l'info "Fd" +// version fast 1000 joules + + /* Pour dériver du surplus solaire vers un chauffe eau utilisant un relai statique non passage à zero. + * + PRINCIPE =Calculer une cinquantaine de fois par période la puissance instantannée + au noend de raccordement;moyenner sur le cycle puis en déduire le paquet d'énergie à ajouter ou soustraire + au "bucket". + A l'intérieur d'une fourchette de 100 à 1000 joules ,un triac est activé plus ou moins tard + dérivant vers une charge extérieure la quantité exacte d'énergie de façon à retorter + ou exporter un minimum. + Les échantillons doivent être décales (offset)et filtrés pour être numérisés par l'Atmel + */ + +#include +#define POSITIVE 1 +#define NEGATIVE 0 +#define ON 1 // commande positive du relai statique ou triac +#define OFF 0 +byte CdeCh1 = 2;// pin4 micro cde SSR1 +byte CdeCh2 = 4;// 8 pin 14 micro cde SSR2 (ou 4 pin 6) +byte voltageSensorPin = A3; +byte currentSensorPin = A5; +float SommeP = 0; //somme de P1 sur N periodes +float ret = 0 ; // retard + +float imaP = 0; //image de P pour calcul de SMC +long cycleCount = 0; +long cptperiodes = 0; +int samplesDuringThisMainsCycle = 0; +byte nextStateOfTriac; +float cyclesPerSecond = 50; // flottant pour plus de précision +int seuilH; //seuil enclenchemen ch2 +int seuilB; //seuil déclenchemen ch2 +int CE ; // puissance Chauffe eau +int KCE ; // coefficient puissance kCE = 8000 / CE +byte polarityNow; + +boolean flg2 = false ; // pulse cde 2 +boolean triggerNeedsToBeArmed = false; +boolean beyondStartUpPhase = false; +float Plissee = 0; +float energyInBucket = 0; +float rPWRprec ; //realPower de la période précédente +int capacityOfEnergyBucket = 1000; // AU LIEU DE 3600 +int recovPWR = 500 ;// VOIR +int sampleV,sampleI; // Les valeurs de tension et courant sont des entiers dans l'espace 0 ...1023 +int lastSampleV; // valeur à la boucle précédente. +float lastFilteredV,filteredV; // tension après filtrage pour retirer la composante continue +float prevDCoffset; // <<--- pour filtre passe bas +float DCoffset; // <<--- idem +float cumVdeltasThisCycle; // <<--- idem +float sampleVminusDC; // <<--- idem +float sampleIminusDC; // <<--- idem +float lastSampleVminusDC; // <<--- idem +float sumP; // Somme cumulée des puissances à l'intérieur d'un cycle. // realpower est la puissance moyenne à l'intérieur d'une période +int PHASECAL; //correction de phase inutilisé = 1 +float POWERCAL; // pour conversion des valeur brutes V et I en joules. +int VOLTAGECAL; // Pour determiner la tension mini de déclenchement du triac. // the trigger device can be safely armed +boolean firstLoopOfHalfCycle; +boolean phaseAngleTriggerActivated; +unsigned long Tc; // = To machine à chaque début de 1/2 periode +unsigned long Fd; // firing delay + +void setup() +{ + wdt_enable(WDTO_8S); + Serial.begin(500000); // pour tests + pinMode(CdeCh1, OUTPUT); + pinMode(CdeCh2, OUTPUT); + +//++++++++ PARAMETRES A MODIFIER SUIVANT INSTALL++++++ + CE = 1800 ; // Puissance chauffe eau + seuilH =500; //seuil enclenchemen ch2 en W approximatifs + seuilB =100; //seuil déclenchemen ch2 +// ++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + KCE = 8000 / (CE) ; // coefficient suivant la puissance du CE pour la définition des seuils + + POWERCAL = 0.18 ; // org 0.12 à ajuster pour faire coincider la puissance vraie avec le realPwer. + // en utilisant le traceur serie. + // NON CRITIQUE car les valeur absolues s'annulent en phase de régulation // retort and export flows are balanced. + + VOLTAGECAL = (float)679 / 471; // En volts par pas d'ADC. + // Utilisé pour déterminer quand la tension secteur est suffisante pour + // exciter le triac. noter les valeurs min et max de la mesure de tension + // par exemple 678.8 pic pic + // La dynamique étant de 471 signifie une sous estimation de la tension + // de 471/679. VOLTAGECAL doit donc être multiplié par l'inverse + // de 679/471 soit 1.44 + PHASECAL = 1; // NON CRITIQUE + + sumP = 0 ; +} + +void loop() // Une paire tension / courant est mesurée à chaque boucle (environ 54 par période) + { + wdt_reset(); + samplesDuringThisMainsCycle++; // incrément du nombre d'échantillons par période secteur pour calcul de puissance. +// Sauvegarde des échantillons précédents + lastSampleV=sampleV; // pour le filtre passe haut + lastFilteredV = filteredV; // afin d'identifier le début de chaque période + lastSampleVminusDC = sampleVminusDC; // for phasecal calculation + +// Acquisition d'une nouvelle paire d'chantillons bruts. temps total :380µS + sampleI = analogRead(currentSensorPin); + sampleV = analogRead(voltageSensorPin); + +// Soustraction de la composante continue déterminée par le filtre passe bas + sampleVminusDC = sampleV - DCoffset; + sampleIminusDC = sampleI - DCoffset; + +// Un filtre passe haut est utilisé pour déterminer le début de cycle. + filteredV = 0.996*(lastFilteredV+sampleV-lastSampleV); // Sinus tension reconstituée +// lastFilteredV = zéro en début de cycle + digitalWrite(CdeCh1, OFF); +// Détection de la polarité de l'alternance + byte polarityOfLastReading = polarityNow; + + if(filteredV >= 0) + polarityNow = POSITIVE; + else + polarityNow = NEGATIVE; + + if (polarityNow == POSITIVE) + { + if (polarityOfLastReading != POSITIVE) + { +// C'est le départ d'une nouvelle sinus positive juste après le passage à zéro + cycleCount++; // incrément Nb de périodes + cptperiodes++; // pour affichage toutes les 50 périodes de Power + firstLoopOfHalfCycle = true; + +// mise à jour du filtre passe bas pour la soustraction de la composante continue + prevDCoffset = DCoffset; + DCoffset = prevDCoffset + (0.015 * cumVdeltasThisCycle); + +// Calcul la puissance réelle de toutes les mesures echantillonnées durant le cycle précedent, et determination du gain (ou la perte) en energie. + float realPower = POWERCAL * sumP / (float)samplesDuringThisMainsCycle; + float realEnergy = realPower / cyclesPerSecond;//Pmoy X 0.02 en joules sur une période + if (beyondStartUpPhase == true)// > 2 secondes + { +// Supposant que les filtres ont eu suffisamment de temps de se stabiliser ; ajout de cette energie d'une période à l'énergie du reservoir. + energyInBucket += realEnergy; + +// Limites dans la fourchette 0 ...1000 joules ;ne peut être négatif le 11 / 2 / 18 + if (energyInBucket > capacityOfEnergyBucket) + energyInBucket = capacityOfEnergyBucket; + if (energyInBucket < 0) + energyInBucket = 0; + } + else + { +// Après un reset attendre 100 périodes (2 secondes) le temps que la composante continue soit éliminée par le filtre + if(cycleCount > 100) // deux secondes + beyondStartUpPhase = true; //croisière + } + triggerNeedsToBeArmed = true; // déclenchement armé à chaque cycle + + // ******************************************************** + // determination du retard de déclenchement du triac + + + if (energyInBucket <= 100) // Ne pas allumer si l'énergie du bucket est trop basse (Fd = Firing Delay) + {Fd = 99999; + } + else + + if (energyInBucket >= 1000) // déclencher immediatement si le niveau d'energie est au dessus du max + { Fd = 200; + } + + // determination du bon point de déclenchement pour un niveau donné + // algorithme simple + // Fd est le retard au déclenchement en microsecondes du début de la sinus . + // pour Pmin = 10000 corrigé à 8500 + // pour Pmax = 0 corrigé à 200 + + else + { + Fd = 10 * (1020 - energyInBucket); + + ret = (Fd); + if (ret >= 8000) + ret = 8000; // LIMITE BASSE 8000 soit 50W + imaP = 8000 - (ret) ; + if (Fd > 8500) // pas de déclenchement + { Fd = 99999; + } + } + //****************************************************** + // imaP des SEUILS imaP est une image de la puissance routée (8000-(Fd))/N environ 1500 W max + + // SommeP tient compte de la puissance du Chauffe eau + // Plissee est SommeP moyennée sur N périodes + + (SommeP += (imaP / (KCE))) ; // Puissance routée par période secteur et incrémentée + + if (cptperiodes==250) //Pmoy lissée ;par exemple sur 250 secondes + + { Plissee = SommeP / 250 ; //moyenne sur N échantillons } + if (Plissee >= seuilH) //seuil enclenchemen ch2 + { flg2 = true ; + } + if (Plissee <= seuilB) // sinon ,on coupe + { flg2 = false ; + } + + // Réinitialisation avant un nouveau moyennage + cptperiodes = 0; + SommeP = 0; + + } + sumP = 0;// somme des puissances instantannées + samplesDuringThisMainsCycle = 0; + cumVdeltasThisCycle = 0;// somme des tensions instantannées filtrées + } // Fin du processus spécifique au premier échantillon +ve d'un nouveau cycle secteur + + // suite du traitement des échantillons de tension POSITIFS ... + + } // Fin du processus sur la demi alternance positive (tension) + else + { + if (polarityOfLastReading != NEGATIVE) + { + firstLoopOfHalfCycle = true; + } + } + // Processus pour TOUS les échantillons, positifs et negatifs 54 fois par période + + + unsigned long To = micros(); // Nb de microsSec depuis le lancement du PG + + if (flg2 == true ) + { {digitalWrite(CdeCh2, ON) ;}} + else + { {digitalWrite(CdeCh2, OFF) ;}} + +if (firstLoopOfHalfCycle == true) + + { Tc = To; // mise à l'heure en début de 1/2 alternance + firstLoopOfHalfCycle = false; + phaseAngleTriggerActivated = false; + // Autre que P max,annuler le déclenchement a la première boucle + // de chaque demi cycle pour être sur qu'il ne reste pas bloqué ON. + if(Fd > 200) + + { digitalWrite(CdeCh1, OFF);} + } + if (phaseAngleTriggerActivated == true) + { + // Sauf demande de puissance max,désarmer le déclenchement a chaque boucle + // après la conduction de la demi période. durée du pulse 20000 / 54 = 370µS + + if (Fd > 200) + { digitalWrite(CdeCh1, OFF); } + } + else + { + if (To >= (Tc + Fd)) + { digitalWrite(CdeCh1, ON); // at To + Firing delay + phaseAngleTriggerActivated = true; } + + } + // Fin de la gestion de l'allumage du Triac + //******************************************************* + + + // Apply phase-shift to the voltage waveform to ensure that the system measures a + // resistive load with a power factor of unity. + float phaseShiftedVminusDC = + lastSampleVminusDC + PHASECAL * (sampleVminusDC - lastSampleVminusDC); + float instP = phaseShiftedVminusDC * sampleIminusDC; // PUISSANCE derniers échantillons V x I filtrés + sumP +=instP; // accumulation à chaque boucle des puissances instantanées dans une période + + cumVdeltasThisCycle += (sampleV - DCoffset); // pour usage avec filtre passe bas + +} // end of loop() diff --git a/docs/routers/Test_sortie_triac_et_zero_crosing.ino b/docs/routers/Test_sortie_triac_et_zero_crosing.ino new file mode 100644 index 0000000..f889678 --- /dev/null +++ b/docs/routers/Test_sortie_triac_et_zero_crosing.ino @@ -0,0 +1,59 @@ +/* + +*/ + +int pushButton =33; + int buttonState = 0; +// the setup function runs once when you press reset or power the board +void setup() { + // pinmode(27, OUTPUT ) = Sortie triac 1 / pinmode(13, OUTPUT ) = Sortie triac 2 + Serial.begin(115200); + pinMode(pushButton, INPUT); + pinMode(27, OUTPUT); + pinMode(13, OUTPUT); +} + +void test_triac(){ + digitalWrite(27, HIGH); + Serial.println ("La sortie S1 TRIAC ON"); + delay(1000); // wait for a second + digitalWrite(27, LOW); // turn the LED off by making the voltage LOW + Serial.println ("La sortie S1 TRIAC OFF"); + delay(500); // wait for a second + digitalWrite(13, HIGH); + Serial.println ("La sortie S2 TRIAC ON"); + delay(1000); // wait for a second + digitalWrite(13, LOW); + Serial.println ("La sortie S2 TRIAC OFF"); + delay(500); // wait for a second +} + + +void test_zeroC(){ + unsigned long start_times; + Serial.println("test Zero crossing :"); + + start_times = millis(); + + while (digitalRead(pushButton) ) if(millis()>start_times+100) return; + while (!digitalRead(pushButton) ) if(millis()>start_times+100) return; + start_times = micros(); + while (digitalRead(pushButton) ); + start_times = micros()- start_times; + + buttonState += 1; + Serial.print ("Compteur Zero crossing :"); + Serial.println(buttonState); + Serial.print ("impulsion Zero crossing : "); + Serial.print(start_times); + Serial.println (" us"); + delay(2000); +} + + +// the loop function runs over and over again forever +void loop() { + test_triac(); + test_zeroC(); + delay(1000); +} diff --git a/docs/routers/mk2pvrouter.co.uk/3phase_cctDiagram_1.pdf b/docs/routers/mk2pvrouter.co.uk/3phase_cctDiagram_1.pdf new file mode 100644 index 0000000..7c1f669 Binary files /dev/null and b/docs/routers/mk2pvrouter.co.uk/3phase_cctDiagram_1.pdf differ diff --git a/docs/routers/mk2pvrouter.co.uk/3phase_rev2_circuit_1.pdf b/docs/routers/mk2pvrouter.co.uk/3phase_rev2_circuit_1.pdf new file mode 100644 index 0000000..074136b Binary files /dev/null and b/docs/routers/mk2pvrouter.co.uk/3phase_rev2_circuit_1.pdf differ diff --git a/docs/routers/mk2pvrouter.co.uk/About_3phase_cctDiagram_1.txt b/docs/routers/mk2pvrouter.co.uk/About_3phase_cctDiagram_1.txt new file mode 100644 index 0000000..f63f7c3 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_3phase_cctDiagram_1.txt @@ -0,0 +1,10 @@ +Notes re. the original circuit diagram for my 3-phase PCB @ rev 1 + +The original circuit diagram shows R1 = 10K which is outside the +specified range for the Atmega 328P processor. In a later version +of the diagram, this value has been increased to 47K. + +In the original circuit diagram, resistors R2 - R4 are shown as 10K. +When operating the processor at 5V, this setup does not make best use +of the available range of the ADC. In a later version of the diagram, +these resistors have been increased to 18K. \ No newline at end of file diff --git a/docs/routers/mk2pvrouter.co.uk/About_3phase_rev2_circuit_1.txt b/docs/routers/mk2pvrouter.co.uk/About_3phase_rev2_circuit_1.txt new file mode 100644 index 0000000..5d2c261 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_3phase_rev2_circuit_1.txt @@ -0,0 +1,3 @@ +Notes re. the original circuit diagram for my 3-phase PCB @ rev 2 + +There are no known problems with this diagram. \ No newline at end of file diff --git a/docs/routers/mk2pvrouter.co.uk/About_JeeLib.txt b/docs/routers/mk2pvrouter.co.uk/About_JeeLib.txt new file mode 100644 index 0000000..308c85b --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_JeeLib.txt @@ -0,0 +1,14 @@ +Note re. JeeLib and the RFM69CW + +If you are using the newer RFM69CW radio module, be sure to change the sketch to suit. +A little way below the initial block of comments, find the 2 lines: + +#define RF69_COMPAT 0 // for the RFM12B +// #define RF69_COMPAT 1 // for the RF69 + +Comment out the first line and un-comment the second, so that it reads: + +// #define RF69_COMPAT 0 // for the RFM12B +#define RF69_COMPAT 1 // for the RF69 + + diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_1.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_1.txt new file mode 100644 index 0000000..fc42578 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_1.txt @@ -0,0 +1,23 @@ +Detail for my Mk2_3phase_RFdatalog_1.ino sketch + +This sketch is intended to run on my "Mk2 3phase" PCB. Voltage and current are +repeatedly sampled for each phase in turn. Every mains cycle, the average powers +for the three separate phases are combined and used to update an "energy bucket". +This "energy bucket" represents the overall energy state of the premises. + +Any surplus energy is made available to dump-loads. This sketch supports +three dump-loads which can be on any phase. The loads are activated in order +of priority. An external switch can be used to select between two pre-set +priority sequences. + +Datalogging is supported. This records the average power and Vrms voltage on +each phase every few seconds. This data is always available at the Serial +interface. If the RF facility is enabled, this data is also transmitted by RF. +RF is enabled by including the literal definition for RF_PRESENT near the top +of the program. If this line is commented out, RF is disabled. + +To minimise the rate at which the loads are cycled on and off, this sketch operates +with a single-threshold anti-flicker algorithm. The optimal rate of cycling is +determined by the supply meter so may need to be adjusted by the user. The rate at +which the loads are cycled on and off can be adjusted using the parameter +postMidPointCrossingDelayForAF_cycles. diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_1a.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_1a.txt new file mode 100644 index 0000000..6659fd0 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_1a.txt @@ -0,0 +1,33 @@ +Detail for my Mk2_3phase_RFdatalog_1.ino sketch + +This sketch is intended to run on my "Mk2 3phase" PCB. Voltage and current are +repeatedly sampled for each phase in turn. Every mains cycle, the average powers +for the three separate phases are combined and used to update an "energy bucket". +This "energy bucket" represents the overall energy state of the premises. + +Any surplus energy is made available to dump-loads. This sketch supports +three dump-loads which can be on any phase. The loads are activated in order +of priority. An external switch can be used to select between two pre-set +priority sequences. + +Datalogging is supported. This records the average power and Vrms voltage on +each phase every few seconds. This data is always available at the Serial +interface. If the RF facility is enabled, this data is also transmitted by RF. +RF is enabled by including the literal definition for RF_PRESENT near the top +of the program. If this line is commented out, RF is disabled. + +To minimise the rate at which the loads are cycled on and off, this sketch operates +with a single-threshold anti-flicker algorithm. The optimal rate of cycling is +determined by the supply meter so may need to be adjusted by the user. The rate at +which the loads are cycled on and off can be adjusted using the parameter +postMidPointCrossingDelayForAF_cycles. + +Changes for version _1a: +When the content of a datalog message is sent to the Serial port, some loss of +data samples is likely to occur. In version 1a, this block of Serial statements +has therefore been commented out. + +A mechanism has been added which monitors the minimum number of sample sets per +mains cycle. At 50Hz operation, the expected value is 32. Whenever a datalog message +is displayed at the Serial port, this value drops to 29 or 30. The correct value +can be seen when the offending Serial statements are disabled. diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_2.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_2.txt new file mode 100644 index 0000000..0beccac --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_2.txt @@ -0,0 +1,30 @@ +Detail for my Mk2_3phase_RFdatalog_1.ino sketch + +This sketch is intended to run on my "Mk2 3phase" PCB. Voltage and current are +repeatedly sampled for each phase in turn. Every mains cycle, the average powers +for the three separate phases are combined and used to update an "energy bucket". +This "energy bucket" represents the overall energy state of the premises. + +Any surplus energy is made available to dump-loads. This sketch supports +three dump-loads which can be on any phase. The loads are activated in order +of priority. An external switch can be used to select between two pre-set +priority sequences. + +Datalogging is supported. This records the average power and Vrms voltage on +each phase every few seconds. If the RF facility is enabled, this data is +transmitted by RF. RF is enabled by including the literal definition for +RF_PRESENT near the top of the sketch. If this line is commented out, RF is disabled. + +The same data can be sent to the Serial interface, but this could potentially disturb +the underlying sampling sequence. + +Changes for version _2: + +- a twin-threshold algorithm for energy state management has been adopted +- improved mechanism for controlling multiple loads (faster and more accurate); +- ISR upgraded to prevent a possible timing anomaly +- a performance checking feature has been added to detect any loss of data +- the RF69 RF module is now supported +- the control signals are now active-high to suit the latest 3-phase PCB. +- SWEETZONE_IN_JOULES has been replaced by WORKING_RANGE_IN_JOULES. + diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_3.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_3.txt new file mode 100644 index 0000000..2e2454d --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_3.txt @@ -0,0 +1,40 @@ +Detail for my Mk2_3phase_RFdatalog_3.ino sketch + +This sketch is intended to run on my "Mk2 3phase" PCB. Voltage and current are +repeatedly sampled for each phase in turn. Every mains cycle, the average powers +for the three separate phases are combined and used to update an "energy bucket". +This "energy bucket" represents the overall energy state of the premises. + +Any surplus energy is made available to dump-loads. This sketch supports +three dump-loads which can be on any phase. The loads are activated in order +of priority. An external switch can be used to select between two pre-set +priority sequences. + +Datalogging is supported. This records the average power and Vrms voltage on +each phase every few seconds. If the RF facility is enabled, this data is +transmitted by RF. RF is enabled by including the literal definition for +RF_PRESENT near the top of the sketch. If this line is commented out, RF is disabled. + +The same data can be sent to the Serial interface, but this could potentially disturb +the underlying sampling sequence. + +Changes for version _2: + +- a twin-threshold algorithm for energy state management has been adopted +- improved mechanism for controlling multiple loads (faster and more accurate); +- ISR upgraded to prevent a possible timing anomaly +- a performance checking feature has been added to detect any loss of data +- the RF69 RF module is now supported +- the control signals are now active-high to suit the latest 3-phase PCB. +- SWEETZONE_IN_JOULES has been replaced by WORKING_RANGE_IN_JOULES. + +Changes for version _3: + - improvements to the start-up logic. The start of normal operation is now + synchronised with the start of a new mains cycle. + - reduce the amount of feedback in the Low Pass Filter for removing the DC content + from the Vsample stream. This resolves an anomaly which has been present since + the start of this project. Although the amount of feedback has previously been + excessive, this anomaly has had minimal effect on the system's overall behaviour. + - The reported power at each of the phases has been inverted. These values are now in + line with the Open Energy Monitor convention, whereby import is positive and + export is negative. diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_3a.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_3a.txt new file mode 100644 index 0000000..11da624 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_3a.txt @@ -0,0 +1,48 @@ +Detail for my Mk2_3phase_RFdatalog_3a.ino sketch + +This sketch is intended to run on my "Mk2 3phase" PCB. Voltage and current are +repeatedly sampled for each phase in turn. Every mains cycle, the average powers +for the three separate phases are combined and used to update an "energy bucket". +This "energy bucket" represents the overall energy state of the premises. + +Any surplus energy is made available to dump-loads. This sketch supports +three dump-loads which can be on any phase. The loads are activated in order +of priority. An external switch can be used to select between two pre-set +priority sequences. + +Datalogging is supported. This records the average power and Vrms voltage on +each phase every few seconds. If the RF facility is enabled, this data is +transmitted by RF. RF is enabled by including the literal definition for +RF_PRESENT near the top of the sketch. If this line is commented out, RF is disabled. + +The same data can be sent to the Serial interface, but this could potentially disturb +the underlying sampling sequence. + +Changes for version _2: + +- a twin-threshold algorithm for energy state management has been adopted +- improved mechanism for controlling multiple loads (faster and more accurate); +- ISR upgraded to prevent a possible timing anomaly +- a performance checking feature has been added to detect any loss of data +- the RF69 RF module is now supported +- the control signals are now active-high to suit the latest 3-phase PCB. +- SWEETZONE_IN_JOULES has been replaced by WORKING_RANGE_IN_JOULES. + +Changes for version _3: + - improvements to the start-up logic. The start of normal operation is now + synchronised with the start of a new mains cycle. + - reduce the amount of feedback in the Low Pass Filter for removing the DC content + from the Vsample stream. This resolves an anomaly which has been present since + the start of this project. Although the amount of feedback has previously been + excessive, this anomaly has had minimal effect on the system's overall behaviour. + - The reported power at each of the phases has been inverted. These values are now in + line with the Open Energy Monitor convention, whereby import is positive and + export is negative. + * + * February 2020: updated to Mk2_3phase_RFdatalog_3a with these changes: + * - removal of some redundant code in the logic for determining the next load state. + * + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ \ No newline at end of file diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_4.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_4.txt new file mode 100644 index 0000000..201d3d1 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_3phase_RFdatalog_4.txt @@ -0,0 +1,44 @@ +Detail for my Mk2_3phase_RFdatalog_4.ino sketch + +/* Mk2_3phase_RFdatalog_4.ino + * + * Issue 1 was released in January 2015. + * + * This sketch provides continuous monitoring of real power on three phases. + * Surplus power is diverted to multiple loads in sequential order. A suitable + * output-stage is required for each load; this can be either triac-based, or a + * Solid State Relay. + * + * Datalogging of real power and Vrms is provided for each phase. + * The presence or absence of the RFM12B needs to be set at compile time + * + * January 2016, renamed as Mk2_3phase_RFdatalog_2 with these changes: + * - Improved control of multiple loads has been imported from the + * equivalent 1-phase sketch, Mk2_multiLoad_wired_6.ino + * - the ISR has been upgraded to fix a possible timing anomaly + * - variables to store ADC samples are now declared as "volatile" + * - for RF69 RF module is now supported + * - a performance check has been added with the result being sent to the Serial port + * - control signals for loads are now active-high to suit the latest 3-phase PCB + * + * February 2016, renamed as Mk2_3phase_RFdatalog_3 with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - The reported power at each of the phases has been inverted. These values are now in + * line with the Open Energy Monitor convention, whereby import is positive and + * export is negative. + * + * February 2020: updated to Mk2_3phase_RFdatalog_3a with these changes: + * - removal of some redundant code in the logic for determining the next load state. + * + * July 2022: updated to Mk2_3phase_RFdatalog_4, with this change: + * - the datalogging accumulator for Vsquared has been rescaled to 1/16 of its previous value + * to avoid the risk of overflowing during a 20-second datalogging period. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_1.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_1.txt new file mode 100644 index 0000000..24c3866 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_1.txt @@ -0,0 +1,15 @@ +Detail for my Mk2_RFdatalog_1.ino sketch + +This is an upgraded version of the original Mk2 sketch for use with my +PCB-based hardware. It supports two current sensors, CT1 and CT2. +CT1 is for monitoring the flow of energy at the supply point; CT2 is +available for monitoring the flow of current to the primary dump-load. + +With this version, datalog messages are routinely transmitted by the +on-board RF facility. By using the pin-saving hardware option, the +4-digit display is still available for use. + +The payload of the transmitted data has three integer fields: +- the message number, which increases by one every time; +- the power at the supply point in Joules (import is -ve, export is +ve) +- the diverted energy total for today, in kWh diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_2.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_2.txt new file mode 100644 index 0000000..59ad171 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_2.txt @@ -0,0 +1,30 @@ +Detail for my Mk2_RFdatalog_2.ino sketch + +This is an upgraded version of the original Mk2 sketch for use with my +PCB-based hardware. It supports two current sensors, CT1 and CT2. +CT1 is for monitoring the flow of energy at the supply point; CT2 is +available for monitoring the flow of current to the primary dump-load. + +Datalog messages are routinely transmitted by the on-board RF facility. +By using the pin-saving hardware option, the 4-digit display is still +available for use. + +The _2 release includes various changes since the _1 version: + +- The transmitted data now only has two integer fields (no message ID): + . the power at the supply point in Joules (import is +ve, export is -ve) + . the diverted energy total for today, in kWh + (note the change in energy sense to match the OEM convention) + +- a REQUIRED_EXPORT_IN_WATTS facility has been added. This acts like a + leak in the energy bucket, therefore reducing the amount of surplus power + that can be diverted. When a negative value is entered, this facility + acts like a PV Simulator, which can be very helful for test purposes. + +- The state of the energy bucket is displayed at the Serial Monitor every + second in Joules. + +- The RFM12B is no longer sent to sleep between RF trnsmissions, it remains + awake throughout. The library call rf12_sendStart() has been replaced by + rf12_sendNow(). These changes are intended to minimise any disruption + to the continuous sampling process when RF transmissions are sent. \ No newline at end of file diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_3.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_3.txt new file mode 100644 index 0000000..567ee82 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_3.txt @@ -0,0 +1,38 @@ +Detail for my Mk2_RFdatalog_3.ino sketch + +This is an upgraded version of the original Mk2 sketch for use with my +PCB-based hardware. It supports two current sensors, CT1 and CT2. +CT1 is for monitoring the flow of energy at the supply point; CT2 is +available for monitoring the flow of current to the primary dump-load. + +Datalog messages are routinely transmitted by the on-board RF facility. +By using the pin-saving hardware option, the 4-digit display is still +available for use. + +The _2 release includes various changes since the _1 version: + +- The transmitted data now only has two integer fields (no message ID): + . the power at the supply point in Joules (import is +ve, export is -ve) + . the diverted energy total for today, in kWh + (note the change in energy sense to match the OEM convention) + +- a REQUIRED_EXPORT_IN_WATTS facility has been added. This acts like a + leak in the energy bucket, therefore reducing the amount of surplus power + that can be diverted. When a negative value is entered, this facility + acts like a PV Simulator, which can be very helful for test purposes. + +- The state of the energy bucket is displayed at the Serial Monitor every + second in Joules. + +- The RFM12B is no longer sent to sleep between RF trnsmissions, it remains + awake throughout. The library call rf12_sendStart() has been replaced by + rf12_sendNow(). These changes are intended to minimise any disruption + to the continuous sampling process when RF transmissions are sent. + +Changes for version _3: + +- For compatibility with other versions of the Mk2 code, the variable cycleCount + has been removed. This variable would have eventually overflowed which + could have caused unpredictable effects with other versions of the Mk2 code. + + diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_4.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_4.txt new file mode 100644 index 0000000..061ed82 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_4.txt @@ -0,0 +1,59 @@ +Detail for my Mk2_RFdatalog_4.ino sketch + +This is an upgraded version of the original Mk2 sketch for use with my +PCB-based hardware. It supports two current sensors, CT1 and CT2. +CT1 is for monitoring the flow of energy at the supply point; CT2 is +available for monitoring the flow of current to the dump-load. + +Datalog messages are routinely transmitted by the on-board RF facility. +By using the pin-saving hardware option, the 4-digit display is still +available for use. + +The _2 release includes various changes since the _1 version: + +- The transmitted data now only has two integer fields (no message ID): + . the power at the supply point in Joules (import is +ve, export is -ve) + . the diverted energy total for today, in kWh + (note the change in energy sense to match the OEM convention) + +- a REQUIRED_EXPORT_IN_WATTS facility has been added. This acts like a + leak in the energy bucket, therefore reducing the amount of surplus power + that can be diverted. When a negative value is entered, this facility + acts like a PV Simulator, which can be very helful for test purposes. + +- The state of the energy bucket is displayed at the Serial Monitor every + second in Joules. + +- The RFM12B is no longer sent to sleep between RF transmissions, it remains + awake throughout. The library call rf12_sendStart() has been replaced by + rf12_sendNow(). These changes are intended to minimise any disruption + to the continuous sampling process when RF transmissions are sent. + +Changes for version _3: + +- For compatibility with other versions of the Mk2 code, the variable cycleCount + has been removed. This variable would have eventually overflowed which + could have caused unpredictable effects with other versions of the Mk2 code. + +Changes for version _4: + +- Code restructured so that all of the main activities are performed within the + Interrupt Service Routine. This includes all of the sampling and power diversion + functions. Only the slower activities are dealt with by the main code, in loop(). + +- A checker mechanism records the minimum number of sample sets per mains cycle. + For 50 Hz, this should always be 63 or 64. {20000us / (104us * 3) = 64.1} + For 60 Hz, I presume it should always be 53. {16666us / (104us * 3) = 53.4} + +- temperature sensing is now supported using a Dallas OneWire sensor at the "mode" port. +- the output mode is now fixed at compile time (unless the "mode" port switch is available) +- the ADC is now free-running rather than being controlled by a fixed rate timer +- a persistence check had been added for the zero-crossing detector +- the payload + check data are displayed to the screen whenever a datalog message is sent + (for more details, please consult the code) + +- In version_2, an inversion was introduced so that the reported power at the + supply point would be in line with the Open Energy Monitor convention (whereby + consumption is positive). Unfortunately, this modification has been lost during + the version_3 to version_4 upgrade. This change can be easily reinstated by + adding after Line #525: tx_data.powerAtSupplyPoint *= -1; diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_4a.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_4a.txt new file mode 100644 index 0000000..d611592 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_4a.txt @@ -0,0 +1,75 @@ +Detail for my Mk2_RFdatalog_4a.ino sketch + +This is an upgraded version of the original Mk2 sketch for use with my +PCB-based hardware. It supports two current sensors, CT1 and CT2. +CT1 is for monitoring the flow of energy at the supply point; CT2 is +available for monitoring the flow of current to the dump-load. + +Datalog messages are routinely transmitted by the on-board RF facility. +By using the pin-saving hardware option, the 4-digit display is still +available for use. + +The _2 release includes various changes since the _1 version: + +- The transmitted data now only has two integer fields (no message ID): + . the power at the supply point in Joules (import is +ve, export is -ve) + . the diverted energy total for today, in kWh + (note the change in energy sense to match the OEM convention) + +- a REQUIRED_EXPORT_IN_WATTS facility has been added. This acts like a + leak in the energy bucket, therefore reducing the amount of surplus power + that can be diverted. When a negative value is entered, this facility + acts like a PV Simulator, which can be very helful for test purposes. + +- The state of the energy bucket is displayed at the Serial Monitor every + second in Joules. + +- The RFM12B is no longer sent to sleep between RF transmissions, it remains + awake throughout. The library call rf12_sendStart() has been replaced by + rf12_sendNow(). These changes are intended to minimise any disruption + to the continuous sampling process when RF transmissions are sent. + +Changes for version _3: + +- For compatibility with other versions of the Mk2 code, the variable cycleCount + has been removed. This variable would have eventually overflowed which + could have caused unpredictable effects with other versions of the Mk2 code. + +Changes for version _4: + +- Code restructured so that all of the main activities are performed within the + Interrupt Service Routine. This includes all of the sampling and power diversion + functions. Only the slower activities are dealt with by the main code, in loop(). + +- A checker mechanism records the minimum number of sample sets per mains cycle. + For 50 Hz, this should always be 63 or 64. {20000us / (104us * 3) = 64.1} + For 60 Hz, I presume it should always be 53. {16666us / (104us * 3) = 53.4} + +- temperature sensing is now supported using a Dallas OneWire sensor at the "mode" port. +- the output mode is now fixed at compile time (unless the "mode" port switch is available) +- the ADC is now free-running rather than being controlled by a fixed rate timer +- a persistence check had been added for the zero-crossing detector +- the payload + check data are displayed to the screen whenever a datalog message is sent + (for more details, please consult the code) + +- In version_2, an inversion was introduced so that the reported power at the + supply point would be in line with the Open Energy Monitor convention (whereby + consumption is positive). Unfortunately, this modification has been lost during + the version_3 to version_4 upgrade. This change can be easily reinstated by + adding after Line #525: tx_data.powerAtSupplyPoint *= -1; + +Changes for version _4a: + +- During the major restructuring from version _3 to _4, two minor problems were introduced. + These have both been fixed by the upgrade to version _4a: + +- In version 4, the phaseCal calculation was ineffective because two assignment statements + within the restructured ISR routine were in the wrong order. The order of these statements + has now been changed so that the phaseCal refinement will again work as intended. + +- The inversion of the reported power at the supply point has been reinstated. The reported + power value is now in line with the Open Energy Monitor convention, whereby import is positive + and export is negative. + +- the display timeout period has been reduced to 8 hours instead of 10. + diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_4b.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_4b.txt new file mode 100644 index 0000000..3dcdd43 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_4b.txt @@ -0,0 +1,80 @@ +Detail for my Mk2_RFdatalog_4b.ino sketch + +This is an upgraded version of the original Mk2 sketch for use with my +PCB-based hardware. It supports two current sensors, CT1 and CT2. +CT1 is for monitoring the flow of energy at the supply point; CT2 is +available for monitoring the flow of current to the dump-load. + +Datalog messages are routinely transmitted by the on-board RF facility. +By using the pin-saving hardware option, the 4-digit display is still +available for use. + +The _2 release includes various changes since the _1 version: + +- The transmitted data now only has two integer fields (no message ID): + . the power at the supply point in Joules (import is +ve, export is -ve) + . the diverted energy total for today, in kWh + (note the change in energy sense to match the OEM convention) + +- a REQUIRED_EXPORT_IN_WATTS facility has been added. This acts like a + leak in the energy bucket, therefore reducing the amount of surplus power + that can be diverted. When a negative value is entered, this facility + acts like a PV Simulator, which can be very helful for test purposes. + +- The state of the energy bucket is displayed at the Serial Monitor every + second in Joules. + +- The RFM12B is no longer sent to sleep between RF transmissions, it remains + awake throughout. The library call rf12_sendStart() has been replaced by + rf12_sendNow(). These changes are intended to minimise any disruption + to the continuous sampling process when RF transmissions are sent. + +Changes for version _3: + +- For compatibility with other versions of the Mk2 code, the variable cycleCount + has been removed. This variable would have eventually overflowed which + could have caused unpredictable effects with other versions of the Mk2 code. + +Changes for version _4: + +- Code restructured so that all of the main activities are performed within the + Interrupt Service Routine. This includes all of the sampling and power diversion + functions. Only the slower activities are dealt with by the main code, in loop(). + +- A checker mechanism records the minimum number of sample sets per mains cycle. + For 50 Hz, this should always be 63 or 64. {20000us / (104us * 3) = 64.1} + For 60 Hz, I presume it should always be 53. {16666us / (104us * 3) = 53.4} + +- temperature sensing is now supported using a Dallas OneWire sensor at the "mode" port. +- the output mode is now fixed at compile time (unless the "mode" port switch is available) +- the ADC is now free-running rather than being controlled by a fixed rate timer +- a persistence check had been added for the zero-crossing detector +- the payload + check data are displayed to the screen whenever a datalog message is sent + (for more details, please consult the code) + +- In version_2, an inversion was introduced so that the reported power at the + supply point would be in line with the Open Energy Monitor convention (whereby + consumption is positive). Unfortunately, this modification has been lost during + the version_3 to version_4 upgrade. This change can be easily reinstated by + adding after Line #525: tx_data.powerAtSupplyPoint *= -1; + +Changes for version _4a: + +- During the major restructuring from version _3 to _4, two minor problems were introduced. + These have both been fixed by the upgrade to version _4a: + +- In version 4, the phaseCal calculation was ineffective because two assignment statements + within the restructured ISR routine were in the wrong order. The order of these statements + has now been changed so that the phaseCal refinement will again work as intended. + +- The inversion of the reported power at the supply point has been reinstated. The reported + power value is now in line with the Open Energy Monitor convention, whereby import is + positive and export is negative. + +- the display timeout period has been reduced to 8 hours instead of 10. + +Changes for version _4b: +- The variables to store copies of ADC data for use by the main code are now declared as + "volatile" to remove any possibility of incorrect operation due to optimisation by the + compiler. + diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_5.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_5.txt new file mode 100644 index 0000000..be621c0 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_5.txt @@ -0,0 +1,90 @@ +Detail for my Mk2_RFdatalog_5.ino sketch + +This is an upgraded version of the original Mk2 sketch for use with my +PCB-based hardware. It supports two current sensors, CT1 and CT2. +CT1 is for monitoring the flow of energy at the supply point; CT2 is +available for monitoring the flow of current to the dump-load. + +Datalog messages are routinely transmitted by the on-board RF facility. +By using the pin-saving hardware option, the 4-digit display is still +available for use. + +The _2 release includes various changes since the _1 version: + +- The transmitted data now only has two integer fields (no message ID): + . the power at the supply point in Joules (import is +ve, export is -ve) + . the diverted energy total for today, in kWh + (note the change in energy sense to match the OEM convention) + +- a REQUIRED_EXPORT_IN_WATTS facility has been added. This acts like a + leak in the energy bucket, therefore reducing the amount of surplus power + that can be diverted. When a negative value is entered, this facility + acts like a PV Simulator, which can be very helful for test purposes. + +- The state of the energy bucket is displayed at the Serial Monitor every + second in Joules. + +- The RFM12B is no longer sent to sleep between RF transmissions, it remains + awake throughout. The library call rf12_sendStart() has been replaced by + rf12_sendNow(). These changes are intended to minimise any disruption + to the continuous sampling process when RF transmissions are sent. + +Changes for version _3: + +- For compatibility with other versions of the Mk2 code, the variable cycleCount + has been removed. This variable would have eventually overflowed which + could have caused unpredictable effects with other versions of the Mk2 code. + +Changes for version _4: + +- Code restructured so that all of the main activities are performed within the + Interrupt Service Routine. This includes all of the sampling and power diversion + functions. Only the slower activities are dealt with by the main code, in loop(). + +- A checker mechanism records the minimum number of sample sets per mains cycle. + For 50 Hz, this should always be 63 or 64. {20000us / (104us * 3) = 64.1} + For 60 Hz, I presume it should always be 53. {16666us / (104us * 3) = 53.4} + +- temperature sensing is now supported using a Dallas OneWire sensor at the "mode" port. +- the output mode is now fixed at compile time (unless the "mode" port switch is available) +- the ADC is now free-running rather than being controlled by a fixed rate timer +- a persistence check had been added for the zero-crossing detector +- the payload + check data are displayed to the screen whenever a datalog message is sent + (for more details, please consult the code) + +- In version_2, an inversion was introduced so that the reported power at the + supply point would be in line with the Open Energy Monitor convention (whereby + consumption is positive). Unfortunately, this modification has been lost during + the version_3 to version_4 upgrade. This change can be easily reinstated by + adding after Line #525: tx_data.powerAtSupplyPoint *= -1; + +Changes for version _4a: + +- During the major restructuring from version _3 to _4, two minor problems were introduced. + These have both been fixed by the upgrade to version _4a: + +- In version 4, the phaseCal calculation was ineffective because two assignment statements + within the restructured ISR routine were in the wrong order. The order of these statements + has now been changed so that the phaseCal refinement will again work as intended. + +- The inversion of the reported power at the supply point has been reinstated. The reported + power value is now in line with the Open Energy Monitor convention, whereby import is + positive and export is negative. + +- the display timeout period has been reduced to 8 hours instead of 10. + +Changes for version _4b: +- The variables to store copies of ADC data for use by the main code are now declared as + "volatile" to remove any possibility of incorrect operation due to optimisation by the + compiler. + +Changes for version _5: + - improvements to the start-up logic. The start of normal operation is now + synchronised with the start of a new mains cycle. + - reduce the amount of feedback in the Low Pass Filter for removing the DC content + from the Vsample stream. This resolves an anomaly which has been present since + the start of this project. Although the amount of feedback has previously been + excessive, this anomaly has had minimal effect on the system's overall behaviour. + - tidying of the "confirmPolarity" logic to make its behaviour more clear + - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + - change "triac" to "load" wherever appropriate diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_5a.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_5a.txt new file mode 100644 index 0000000..fc4e014 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_5a.txt @@ -0,0 +1,101 @@ +Detail for my Mk2_RFdatalog_5a.ino sketch + +This is an upgraded version of the original Mk2 sketch for use with my +PCB-based hardware. It supports two current sensors, CT1 and CT2. +CT1 is for monitoring the flow of energy at the supply point; CT2 is +available for monitoring the flow of current to the dump-load. + +Datalog messages are routinely transmitted by the on-board RF facility. +By using the pin-saving hardware option, the 4-digit display is still +available for use. + +The _2 release includes various changes since the _1 version: + +- The transmitted data now only has two integer fields (no message ID): + . the power at the supply point in Joules (import is +ve, export is -ve) + . the diverted energy total for today, in kWh + (note the change in energy sense to match the OEM convention) + +- a REQUIRED_EXPORT_IN_WATTS facility has been added. This acts like a + leak in the energy bucket, therefore reducing the amount of surplus power + that can be diverted. When a negative value is entered, this facility + acts like a PV Simulator, which can be very helful for test purposes. + +- The state of the energy bucket is displayed at the Serial Monitor every + second in Joules. + +- The RFM12B is no longer sent to sleep between RF transmissions, it remains + awake throughout. The library call rf12_sendStart() has been replaced by + rf12_sendNow(). These changes are intended to minimise any disruption + to the continuous sampling process when RF transmissions are sent. + +Changes for version _3: + +- For compatibility with other versions of the Mk2 code, the variable cycleCount + has been removed. This variable would have eventually overflowed which + could have caused unpredictable effects with other versions of the Mk2 code. + +Changes for version _4: + +- Code restructured so that all of the main activities are performed within the + Interrupt Service Routine. This includes all of the sampling and power diversion + functions. Only the slower activities are dealt with by the main code, in loop(). + +- A checker mechanism records the minimum number of sample sets per mains cycle. + For 50 Hz, this should always be 63 or 64. {20000us / (104us * 3) = 64.1} + For 60 Hz, I presume it should always be 53. {16666us / (104us * 3) = 53.4} + +- temperature sensing is now supported using a Dallas OneWire sensor at the "mode" port. +- the output mode is now fixed at compile time (unless the "mode" port switch is available) +- the ADC is now free-running rather than being controlled by a fixed rate timer +- a persistence check had been added for the zero-crossing detector +- the payload + check data are displayed to the screen whenever a datalog message is sent + (for more details, please consult the code) + +- In version_2, an inversion was introduced so that the reported power at the + supply point would be in line with the Open Energy Monitor convention (whereby + consumption is positive). Unfortunately, this modification has been lost during + the version_3 to version_4 upgrade. This change can be easily reinstated by + adding after Line #525: tx_data.powerAtSupplyPoint *= -1; + +Changes for version _4a: + +- During the major restructuring from version _3 to _4, two minor problems were introduced. + These have both been fixed by the upgrade to version _4a: + +- In version 4, the phaseCal calculation was ineffective because two assignment statements + within the restructured ISR routine were in the wrong order. The order of these statements + has now been changed so that the phaseCal refinement will again work as intended. + +- The inversion of the reported power at the supply point has been reinstated. The reported + power value is now in line with the Open Energy Monitor convention, whereby import is + positive and export is negative. + +- the display timeout period has been reduced to 8 hours instead of 10. + +Changes for version _4b: +- The variables to store copies of ADC data for use by the main code are now declared as + "volatile" to remove any possibility of incorrect operation due to optimisation by the + compiler. + +Changes for version _5: + - improvements to the start-up logic. The start of normal operation is now + synchronised with the start of a new mains cycle. + - reduce the amount of feedback in the Low Pass Filter for removing the DC content + from the Vsample stream. This resolves an anomaly which has been present since + the start of this project. Although the amount of feedback has previously been + excessive, this anomaly has had minimal effect on the system's overall behaviour. + - tidying of the "confirmPolarity" logic to make its behaviour more clear + - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + - change "triac" to "load" wherever appropriate + +Changes for version _5a: + - The RF capability is now switchable so that the code will continue to run when an + RF module is not fitted. Dataloging can then still place via the Serial port. + If the RF module is not accessed correctly, the time-critical logic in the ISR will + continue to run in the normal manner. However, the main code will wait forever for a + reply which never appears. This will prevent any progress with the RF, Serial or + temerature measurements. Surplus power can still be diverted within the ISR to the + local load via IO4 at the "trigger" port. + + diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_6.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_6.txt new file mode 100644 index 0000000..3af8352 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_6.txt @@ -0,0 +1,109 @@ +Detail for my Mk2_RFdatalog_6.ino sketch + +This is an upgraded version of the original Mk2 sketch for use with my +PCB-based hardware. It supports two current sensors, CT1 and CT2. +CT1 is for monitoring the flow of energy at the supply point; CT2 is +available for monitoring the flow of current to the dump-load. + +Datalog messages are routinely transmitted by the on-board RF facility. +By using the pin-saving hardware option, the 4-digit display is still +available for use. + +The _2 release includes various changes since the _1 version: + +- The transmitted data now only has two integer fields (no message ID): + . the power at the supply point in Joules (import is +ve, export is -ve) + . the diverted energy total for today, in kWh + (note the change in energy sense to match the OEM convention) + +- a REQUIRED_EXPORT_IN_WATTS facility has been added. This acts like a + leak in the energy bucket, therefore reducing the amount of surplus power + that can be diverted. When a negative value is entered, this facility + acts like a PV Simulator, which can be very helful for test purposes. + +- The state of the energy bucket is displayed at the Serial Monitor every + second in Joules. + +- The RFM12B is no longer sent to sleep between RF transmissions, it remains + awake throughout. The library call rf12_sendStart() has been replaced by + rf12_sendNow(). These changes are intended to minimise any disruption + to the continuous sampling process when RF transmissions are sent. + +Changes for version _3: + +- For compatibility with other versions of the Mk2 code, the variable cycleCount + has been removed. This variable would have eventually overflowed which + could have caused unpredictable effects with other versions of the Mk2 code. + +Changes for version _4: + +- Code restructured so that all of the main activities are performed within the + Interrupt Service Routine. This includes all of the sampling and power diversion + functions. Only the slower activities are dealt with by the main code, in loop(). + +- A checker mechanism records the minimum number of sample sets per mains cycle. + For 50 Hz, this should always be 63 or 64. {20000us / (104us * 3) = 64.1} + For 60 Hz, I presume it should always be 53. {16666us / (104us * 3) = 53.4} + +- temperature sensing is now supported using a Dallas OneWire sensor at the "mode" port. +- the output mode is now fixed at compile time (unless the "mode" port switch is available) +- the ADC is now free-running rather than being controlled by a fixed rate timer +- a persistence check had been added for the zero-crossing detector +- the payload + check data are displayed to the screen whenever a datalog message is sent + (for more details, please consult the code) + +- In version_2, an inversion was introduced so that the reported power at the + supply point would be in line with the Open Energy Monitor convention (whereby + consumption is positive). Unfortunately, this modification has been lost during + the version_3 to version_4 upgrade. This change can be easily reinstated by + adding after Line #525: tx_data.powerAtSupplyPoint *= -1; + +Changes for version _4a: + +- During the major restructuring from version _3 to _4, two minor problems were introduced. + These have both been fixed by the upgrade to version _4a: + +- In version 4, the phaseCal calculation was ineffective because two assignment statements + within the restructured ISR routine were in the wrong order. The order of these statements + has now been changed so that the phaseCal refinement will again work as intended. + +- The inversion of the reported power at the supply point has been reinstated. The reported + power value is now in line with the Open Energy Monitor convention, whereby import is + positive and export is negative. + +- the display timeout period has been reduced to 8 hours instead of 10. + +Changes for version _4b: +- The variables to store copies of ADC data for use by the main code are now declared as + "volatile" to remove any possibility of incorrect operation due to optimisation by the + compiler. + +Changes for version _5: + - improvements to the start-up logic. The start of normal operation is now + synchronised with the start of a new mains cycle. + - reduce the amount of feedback in the Low Pass Filter for removing the DC content + from the Vsample stream. This resolves an anomaly which has been present since + the start of this project. Although the amount of feedback has previously been + excessive, this anomaly has had minimal effect on the system's overall behaviour. + - tidying of the "confirmPolarity" logic to make its behaviour more clear + - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + - change "triac" to "load" wherever appropriate + +Changes for version _5a: + - The RF capability is now switchable so that the code will continue to run when an + RF module is not fitted. Dataloging can then still place via the Serial port. + If the RF module is not accessed correctly, the time-critical logic in the ISR will + continue to run in the normal manner. However, the main code will wait forever for a + reply which never appears. This will prevent any progress with the RF, Serial or + temerature measurements. Surplus power can still be diverted within the ISR to the + local load via IO4 at the "trigger" port. + +Changes for version 6: + * - the parameter cycleCountForDatalogging is now an "int" rather that a "byte". This + * allows the datalogging period to be extended beyond 5 seconds without the counter + * running out of range. + * - diverted energy, as monitored by CT2, is now reported as an average power as well as + * the cumulative energy total for the current day. + + + diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_7.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_7.txt new file mode 100644 index 0000000..c6dbfcc --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_7.txt @@ -0,0 +1,87 @@ +Detail for my Mk2_RFdatalog_7.ino sketch + + * This sketch is for diverting suplus PV power to a dump load using a triac + * or Solid State Relay. Routine datalogging is also supported using the + * on-board RF module (either RFM12B or RF69). + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The integral voltage sensor is fed from one of the secondary coils of the + * transformer. Current is measured via Current Transformers at the + * CT1 and CT1 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. When the RFM12B module is + * in use, the display can only be used in conjunction with an extra pair of + * logic chips. These are ICs 3 and 4, which reduce the number of processor pins + * that are needed to drive the display. + * + * This sketch is based on the Mk2i PV Router code that I have posted in on the + * OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * September 2014: renamed as Mk2_RFdatalog_3, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - tidier initialisation of display logic in setup(); + * + * December 2014, upgraded to Mk2_RFdatalog_4: + * This sketch has been restructured in order to make better use of the ISR. All of + * the time-critical code is now contained within the ISR and its helper functions. + * Values for datalogging are transferred to the main code using a flag-based handshake + * mechanism. The diversion of surplus power can no longer be affected by slower + * activities which may be running in the main code such as Serial statements and RF. + * Temperature sensing is supported by re-allocating the "mode" port for this + * purpose. A pullup resistor (4K7 or similar) is required for the Dallas sensor. The + * output mode, i.e. NORMAL or ANTI_FLICKER, is now set at compile time. + * Also: + * - The ADC is now in free-running mode, at ~104 us per conversion. + * - a persistence check has been added for zero-crossing detection (polarityConfirmed) + * - a lowestNoOfSampleSetsPerMainsCycle check has been added, to detect any disturbances + * - Vrms has been added to the datalog payload (as Vrms x 100) + * - temperature has been added to the datalog payload (as degrees C x 100) + * - the phaseCal mechanism has been reinstated + * + * January 2016: renamed as Mk2_RFdatalog_4a, with a minor change in the ISR to reinstate + * the phaseCal calculation. Previously, this feature was having no effect because + * two assignment lines were in the wrong order. When measuring "real power", which + * is what this application does, the phaseCal refinement has very little effect even + * when correctly implemented, as it now is. + * Support for the RF69 RF module has also been added. + * + * January 2016: updated to Mk2_RFdatalog_4b: + * The variables to store copies of ADC results for use by the main code are now declared + * as "volatile" to remove any possibility of incorrect operation due to optimisation + * by the compiler. + * + * February 2016: updated to Mk2_RFdatalog_5, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * March 2016: updated to Mk2_RFdatalog_5a, with this change: + * - RF capability made switchable so that the code will continue to run + * when an RF module is not fitted. Dataloging can then take place + * via the Serial port. + * + * November 2020: updated to Mk2_RFdatalog_6, with these changes: + * - the parameter cycleCountForDatalogging is now an "int" rather that a "byte". This + * allows the datalogging period to be extended beyond 5 seconds without the counter + * running out of range. + * - diverted energy, as monitored by CT2, is now reported as an average power as well as + * the cumulative energy total for the current day. + * + * July 2022: updated to Mk2_RFdatalog_7, with this change: + * - the datalogging accumulators for grid power, diverted power and Vsquared have been rescaled + * to 1/16 of their previous values to avoid the risk of overflowing during a 10-second + * datalogging period. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_multiLoad_1.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_multiLoad_1.txt new file mode 100644 index 0000000..07721bf --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_multiLoad_1.txt @@ -0,0 +1,109 @@ +Detail for my Mk2_RFdatalog_multiLoad_1.ino sketch + +This sketch is based on Mk2_RFdatalog rev 5a for which the +immediately following notes are relevant: + +This is an upgraded version of the original Mk2 sketch for use with my +PCB-based hardware. It supports two current sensors, CT1 and CT2. +CT1 is for monitoring the flow of energy at the supply point; CT2 is +available for monitoring the flow of current to the dump-load. + +Datalog messages are routinely transmitted by the on-board RF facility. +By using the pin-saving hardware option, the 4-digit display is still +available for use. + +The _2 release includes various changes since the _1 version: + +- The transmitted data now only has two integer fields (no message ID): + . the power at the supply point in Joules (import is +ve, export is -ve) + . the diverted energy total for today, in kWh + (note the change in energy sense to match the OEM convention) + +- a REQUIRED_EXPORT_IN_WATTS facility has been added. This acts like a + leak in the energy bucket, therefore reducing the amount of surplus power + that can be diverted. When a negative value is entered, this facility + acts like a PV Simulator, which can be very helful for test purposes. + +- The state of the energy bucket is displayed at the Serial Monitor every + second in Joules. + +- The RFM12B is no longer sent to sleep between RF transmissions, it remains + awake throughout. The library call rf12_sendStart() has been replaced by + rf12_sendNow(). These changes are intended to minimise any disruption + to the continuous sampling process when RF transmissions are sent. + +Changes for version _3: + +- For compatibility with other versions of the Mk2 code, the variable cycleCount + has been removed. This variable would have eventually overflowed which + could have caused unpredictable effects with other versions of the Mk2 code. + +Changes for version _4: + +- Code restructured so that all of the main activities are performed within the + Interrupt Service Routine. This includes all of the sampling and power diversion + functions. Only the slower activities are dealt with by the main code, in loop(). + +- A checker mechanism records the minimum number of sample sets per mains cycle. + For 50 Hz, this should always be 63 or 64. {20000us / (104us * 3) = 64.1} + For 60 Hz, I presume it should always be 53. {16666us / (104us * 3) = 53.4} + +- temperature sensing is now supported using a Dallas OneWire sensor at the "mode" port. +- the output mode is now fixed at compile time (unless the "mode" port switch is available) +- the ADC is now free-running rather than being controlled by a fixed rate timer +- a persistence check had been added for the zero-crossing detector +- the payload + check data are displayed to the screen whenever a datalog message is sent + (for more details, please consult the code) + +- In version_2, an inversion was introduced so that the reported power at the + supply point would be in line with the Open Energy Monitor convention (whereby + consumption is positive). Unfortunately, this modification has been lost during + the version_3 to version_4 upgrade. This change can be easily reinstated by + adding after Line #525: tx_data.powerAtSupplyPoint *= -1; + +Changes for version _4a: + +- During the major restructuring from version _3 to _4, two minor problems were introduced. + These have both been fixed by the upgrade to version _4a: + +- In version 4, the phaseCal calculation was ineffective because two assignment statements + within the restructured ISR routine were in the wrong order. The order of these statements + has now been changed so that the phaseCal refinement will again work as intended. + +- The inversion of the reported power at the supply point has been reinstated. The reported + power value is now in line with the Open Energy Monitor convention, whereby import is + positive and export is negative. + +- the display timeout period has been reduced to 8 hours instead of 10. + +Changes for version _4b: +- The variables to store copies of ADC data for use by the main code are now declared as + "volatile" to remove any possibility of incorrect operation due to optimisation by the + compiler. + +Changes for version _5: + - improvements to the start-up logic. The start of normal operation is now + synchronised with the start of a new mains cycle. + - reduce the amount of feedback in the Low Pass Filter for removing the DC content + from the Vsample stream. This resolves an anomaly which has been present since + the start of this project. Although the amount of feedback has previously been + excessive, this anomaly has had minimal effect on the system's overall behaviour. + - tidying of the "confirmPolarity" logic to make its behaviour more clear + - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + - change "triac" to "load" wherever appropriate + +Changes for version _5a: + - The RF capability is now switchable so that the code will continue to run when an + RF module is not fitted. Dataloging can then still place via the Serial port. + If the RF module is not accessed correctly, the time-critical logic in the ISR will + continue to run in the normal manner. However, the main code will wait forever for a + reply which never appears. This will prevent any progress with the RF, Serial or + temerature measurements. Surplus power can still be diverted within the ISR to the + local load via IO4 at the "trigger" port. + +Changes for Mk2_RFdatalog_multiLoad_1: +- support for temperature sensing commented out; +- support for an extra load added; +- load priority code added but commented out as all IO ports are in use. + + diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_multiLoad_1a.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_multiLoad_1a.txt new file mode 100644 index 0000000..0de56ce --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_RFdatalog_multiLoad_1a.txt @@ -0,0 +1,84 @@ +Detail for my sketch, Mk2_RFdatalog_multiLoad_1a.ino + * + * This sketch is for diverting suplus PV power to one or two dump loads using + * triac-based output stages or Solid State Relays. Routine datalogging is + * avalable if a suitable RF module is fitted (either RFM12B or RF69). A 4-digit display + * showing the Diverted Energy each day is also supported. + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The integral voltage sensor is fed from one of the secondary coils of the + * transformer. Current is measured via Current Transformers at the + * CT1 and CT1 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. When the RF module is + * in use, the display can only be used in conjunction with an extra pair of + * logic chips. These are ICs 3 and 4, which reduce the number of processor pins + * that are needed to drive the display. + * + * This sketch is based on the Mk2i PV Router code that I have posted in on the + * OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * September 2014: renamed as Mk2_RFdatalog_3, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - tidier initialisation of display logic in setup(); + * + * December 2014, upgraded to Mk2_RFdatalog_4: + * This sketch has been restructured in order to make better use of the ISR. All of + * the time-critical code is now contained within the ISR and its helper functions. + * Values for datalogging are transferred to the main code using a flag-based handshake + * mechanism. The diversion of surplus power can no longer be affected by slower + * activities which may be running in the main code such as Serial statements and RF. + * Temperature sensing is supported by re-allocating the "mode" port for this + * purpose. A pullup resistor (4K7 or similar) is required for the Dallas sensor. The + * output mode, i.e. NORMAL or ANTI_FLICKER, is now set at compile time. + * Also: + * - The ADC is now in free-running mode, at ~104 us per conversion. + * - a persistence check has been added for zero-crossing detection (polarityConfirmed) + * - a lowestNoOfSampleSetsPerMainsCycle check has been added, to detect any disturbances + * - Vrms has been added to the datalog payload (as Vrms x 100) + * - temperature has been added to the datalog payload (as degrees C x 100) + * - the phaseCal mechanism has been reinstated + * + * January 2016: renamed as Mk2_RFdatalog_4a, with a minor change in the ISR to reinstate + * the phaseCal calculation. Previously, this feature was having no effect because + * two assignment lines were in the wrong order. When measuring "real power", which + * is what this application does, the phaseCal refinement has very little effect even + * when correctly implemented, as it now is. + * Support for the RF69 RF module has also been added. + * + * January 2016: updated to Mk2_RFdatalog_4b: + * The variables to store copies of ADC results for use by the main code are now declared + * as "volatile" to remove any possibility of incorrect operation due to optimisation + * by the compiler. + * + * February 2016: updated to Mk2_RFdatalog_5, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * March 2016: updated to Mk2_RFdatalog_5a, with this change: + * - RF capability made switchable so that the code will continue to run + * when an RF module is not fitted. Dataloging can then take place + * via the Serial port. + * + * October 2017: updated to Mk2_RFdatalog_multiLoad_1, with these changes: + * - temperature sensing commented out(formally supported via D3 at the "mode" port") + * - support for a second load added (vcontrolled via D3 at the "mode" port") + * + * + * February 2020: updated to Mk2_RFdatalog_multiLoad_1a with these changes: + * - removal of some redundant code in the logic for determining the next load state. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ \ No newline at end of file diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_1.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_1.txt new file mode 100644 index 0000000..6a89247 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_1.txt @@ -0,0 +1,20 @@ +Detail for my Mk2_bothDisplays_1.ino sketch + +This is the original sketch for use with by PCB-based hardware. +It supports two current sensors. CT1 is for monitoring the flow of +energy at the supply point. CT2 is available for monitoring the flow +of current to the dump-load. + +The display can be driven in either of two ways. A #define statement +near the top of the sketch can be either included or commented out to +achieve this selection. If no display is in use, it doesn't matter +which way the display code is configured; the driver logic will just +rattle away in the background, un-noticed by the rest of the world. + +If a complete system is purchased from me, this is the sketch that will +be pre-loaded into its processor. Only the powerCal value(s) will have +been changed to match the associated hardware. + +The two powerCal variables provides a convenient means of calibrating +hardware for use with a Mk2 Router. CT1 and CT2 each have separate +powerCal variables, with the suffices _grid and _diverted respectively. \ No newline at end of file diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_2.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_2.txt new file mode 100644 index 0000000..9361b2f --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_2.txt @@ -0,0 +1,26 @@ +Detail for my Mk2_bothDisplays_2.ino sketch + +This is an tidier version of the original sketch for use with my +PCB-based hardware. It supports two current sensors. CT1 is +for monitoring the flow of energy at the supply point. CT2 is +available for monitoring the flow of current to the dump-load. + +The display can be driven in either of two ways. A #define statement +near the top of the sketch can be either included or commented out to +achieve this selection. If no display is in use, it doesn't matter +which way the display code is configured; the driver logic will just +rattle away in the background, un-noticed by the rest of the world. + +The two powerCal variables provides a convenient means of calibrating +hardware for use with a Mk2 Router. CT1 and CT2 each have separate +powerCal variables, with the suffices _grid and _diverted respectively. + +Changes for version _2: +- for compatibility with other versions of the Mk2 code, the variable cycleCount +has been removed. This variable would have eventually overflowed which +could have caused unpredictable effects with other versions of the Mk2 code. + +- improved description of the display code initialisation in setup() for the +pin-saving hardware option. + +- removal of some unhelpful comments in the IO pin declaration section. \ No newline at end of file diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_3.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_3.txt new file mode 100644 index 0000000..f4eaf02 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_3.txt @@ -0,0 +1,35 @@ +Detail for my Mk2_bothDisplays_3.ino sketch + +This is an updated version of the original sketch for use with my +PCB-based hardware. It supports two current sensors. CT1 is +for monitoring the flow of energy at the supply point. CT2 is +available for monitoring the flow of current to the dump-load. + +The display can be driven in either of two ways. The #define statement +PIN_SAVING_HARDWARE near the top of the sketch can be either +included or commented out to achieve this selection. If no display is in +use, it doesn't matter which way this line is included or not. + +The two powerCal variables provides a convenient means of calibrating +hardware for use with a Mk2 Router. CT1 and CT2 each have separate +powerCal variables, with the suffices _grid and _diverted respectively. + +Changes for version _2: +- for compatibility with other versions of the Mk2 code, the variable cycleCount +has been removed. This variable would have eventually overflowed which +could have caused unpredictable effects with other versions of the Mk2 code. + +- improved description of the display code initialisation in setup() for the +pin-saving hardware option. + +- removal of some unhelpful comments in the IO pin declaration section. + +Changes for version _3: +- a persistence check for the zero-crossing detection has been added. This +is to remove any false detections of zero-crossings. This effect is seen more +with some types of transformer than others. + +- a mechanism has been added to monitor and display the minimum number +of sample sets that occur each mains cycle. With a 125us timebase, and three +ADC samples per set, the expected number of sample sets per 20ms mains cycle +is 20 / (3 * 0.125) = 53.33. Any value less than 53 would indicate a loss of data. diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_3a.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_3a.txt new file mode 100644 index 0000000..a99f184 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_3a.txt @@ -0,0 +1,38 @@ +Detail for my Mk2_bothDisplays_3a.ino sketch + +This is an updated version of the original sketch for use with my +PCB-based hardware. It supports two current sensors. CT1 is +for monitoring the flow of energy at the supply point. CT2 is +available for monitoring the flow of current to the dump-load. + +The display can be driven in either of two ways. The #define statement +PIN_SAVING_HARDWARE near the top of the sketch can be either +included or commented out to achieve this selection. If no display is in +use, it doesn't matter which way this line is included or not. + +The two powerCal variables provides a convenient means of calibrating +hardware for use with a Mk2 Router. CT1 and CT2 each have separate +powerCal variables, with the suffices _grid and _diverted respectively. + +Changes for version _2: +- for compatibility with other versions of the Mk2 code, the variable cycleCount +has been removed. This variable would have eventually overflowed which +could have caused unpredictable effects with other versions of the Mk2 code. + +- improved description of the display code initialisation in setup() for the +pin-saving hardware option. + +- removal of some unhelpful comments in the IO pin declaration section. + +Changes for version _3: +- a persistence check for the zero-crossing detection has been added. This +is to remove any false detections of zero-crossings. This effect is seen more +with some types of transformer than others. + +- a mechanism has been added to monitor and display the minimum number +of sample sets that occur each mains cycle. With a 125us timebase, and three +ADC samples per set, the expected number of sample sets per 20ms mains cycle +is 20 / (3 * 0.125) = 53.33. Any value less than 53 would indicate a loss of data. + +Version _3a is for a few typographical changes only. + diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_3b.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_3b.txt new file mode 100644 index 0000000..47f49a3 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_3b.txt @@ -0,0 +1,55 @@ +Detail for my Mk2_bothDisplays_3b.ino sketch + +This is an updated version of the original sketch for use with my +PCB-based hardware. It supports two current sensors. CT1 is +for monitoring the flow of energy at the supply point. CT2 is +available for monitoring the flow of current to the dump-load. + +The display can be driven in either of two ways. The #define statement +PIN_SAVING_HARDWARE near the top of the sketch can be either +included or commented out to achieve this selection. If no display is in +use, it doesn't matter whether this line is included or not. + +The two powerCal variables provides a convenient means of calibrating +hardware for use with a Mk2 Router. CT1 and CT2 each have separate +powerCal variables, with the suffices _grid and _diverted respectively. + + +Changes for version _2: +- for compatibility with other versions of the Mk2 code, the variable cycleCount +has been removed. This variable would have eventually overflowed which +could have caused unpredictable effects with other versions of the Mk2 code. + +- improved description of the display code initialisation in setup() for the +pin-saving hardware option. + +- removal of some unhelpful comments in the IO pin declaration section. + + +Changes for version _3: +- a persistence check for the zero-crossing detection has been added. This +is to remove any false detections of zero-crossings. This effect is seen more +with some types of transformer than others. + +- a mechanism has been added to monitor and display the minimum number +of sample sets that occur each mains cycle. With a 125us timebase, and three +ADC samples per set, the expected number of sample sets per 20ms mains cycle +is 20 / (3 * 0.125) = 53.33. Any value less than 53 would indicate a loss +of data. The measured value is sent to the Serial port every 5 seconds. + + +Changes for version _3a: +- typographical changes only. + + +Changes for version _3b: +- A minor change has been made to the function timerIsr() so as to resolve +a timing anomaly that has previously existed. With the new arrangement, a +complete set of data samples is made available by the ISR for use by the +main code. There is no longer any possibility of these values being +overwritten before they are processed. + +- the display timeout period has been reduced to 8 hours instead of 10. + + + diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_3c.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_3c.txt new file mode 100644 index 0000000..bad70ba --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_3c.txt @@ -0,0 +1,59 @@ +Detail for my Mk2_bothDisplays_3c.ino sketch + +This is an updated version of the original sketch for use with my +PCB-based hardware. It supports two current sensors. CT1 is +for monitoring the flow of energy at the supply point. CT2 is +available for monitoring the flow of current to the dump-load. + +The display can be driven in either of two ways. The #define statement +PIN_SAVING_HARDWARE near the top of the sketch can be either +included or commented out to achieve this selection. If no display is in +use, it doesn't matter whether this line is included or not. + +The two powerCal variables provides a convenient means of calibrating +hardware for use with a Mk2 Router. CT1 and CT2 each have separate +powerCal variables, with the suffices _grid and _diverted respectively. + + +Changes for version _2: +- for compatibility with other versions of the Mk2 code, the variable cycleCount +has been removed. This variable would have eventually overflowed which +could have caused unpredictable effects with other versions of the Mk2 code. + +- improved description of the display code initialisation in setup() for the +pin-saving hardware option. + +- removal of some unhelpful comments in the IO pin declaration section. + + +Changes for version _3: +- a persistence check for the zero-crossing detection has been added. This +is to remove any false detections of zero-crossings. This effect is seen more +with some types of transformer than others. + +- a mechanism has been added to monitor and display the minimum number +of sample sets that occur each mains cycle. With a 125us timebase, and three +ADC samples per set, the expected number of sample sets per 20ms mains cycle +is 20 / (3 * 0.125) = 53.33. Any value less than 53 would indicate a loss +of data. The measured value is sent to the Serial port every 5 seconds. + + +Changes for version _3a: +- typographical changes only. + + +Changes for version _3b: +- A minor change has been made to the function timerIsr() so as to resolve +a timing anomaly that has previously existed. With the new arrangement, a +complete set of data samples is made available by the ISR for use by the +main code. There is no longer any possibility of these values being +overwritten before they are processed. + +- the display timeout period has been reduced to 8 hours instead of 10. + + +Changes for version _3c: +- The variables to store ADC data are now declared as "volatile" to remove +any possibility of incorrect operation due to optimisation by the compiler. + + diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_4.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_4.txt new file mode 100644 index 0000000..acf2946 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_bothDisplays_4.txt @@ -0,0 +1,73 @@ +Detail for my Mk2_bothDisplays_4.ino sketch + +This is an updated version of the original sketch for use with my +PCB-based hardware. It supports two current sensors. CT1 is +for monitoring the flow of energy at the supply point. CT2 is +available for monitoring the flow of current to the dump-load. + +The display can be driven in either of two ways. The #define statement +PIN_SAVING_HARDWARE near the top of the sketch can be either +included or commented out to achieve this selection. If no display is in +use, it doesn't matter whether this line is included or not. + +The two powerCal variables provides a convenient means of calibrating +hardware for use with a Mk2 Router. CT1 and CT2 each have separate +powerCal variables, with the suffices _grid and _diverted respectively. + + +Changes for version _2: +- for compatibility with other versions of the Mk2 code, the variable cycleCount +has been removed. This variable would have eventually overflowed which +could have caused unpredictable effects with other versions of the Mk2 code. + +- improved description of the display code initialisation in setup() for the +pin-saving hardware option. + +- removal of some unhelpful comments in the IO pin declaration section. + + +Changes for version _3: +- a persistence check for the zero-crossing detection has been added. This +is to remove any false detections of zero-crossings. This effect is seen more +with some types of transformer than others. + +- a mechanism has been added to monitor and display the minimum number +of sample sets that occur each mains cycle. With a 125us timebase, and three +ADC samples per set, the expected number of sample sets per 20ms mains cycle +is 20 / (3 * 0.125) = 53.33. Any value less than 53 would indicate a loss +of data. The measured value is sent to the Serial port every 5 seconds. + + +Changes for version _3a: +- typographical changes only. + + +Changes for version _3b: +- A minor change has been made to the function timerIsr() so as to resolve +a timing anomaly that has previously existed. With the new arrangement, a +complete set of data samples is made available by the ISR for use by the +main code. There is no longer any possibility of these values being +overwritten before they are processed. + +- the display timeout period has been reduced to 8 hours instead of 10. + + +Changes for version _3c: +- The variables to store ADC data are now declared as "volatile" to remove +any possibility of incorrect operation due to optimisation by the compiler. + +Changes for version _4: + - improvements to the start-up logic. The start of normal operation is now + synchronised with the start of a new mains cycle. + - reduce the amount of feedback in the Low Pass Filter for removing the DC content + from the Vsample stream. This resolves an anomaly which has been present since + the start of this project. Although the amount of feedback has previously been + excessive, this anomaly has had minimal effect on the system's overall behaviour. + - removal of the unhelpful "triggerNeedsToBeArmed" mechanism + - tidying of the "confirmPolarity" logic to make its behaviour more clear + - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + - change "triac" to "load" wherever appropriate + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_1.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_1.txt new file mode 100644 index 0000000..30f747f --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_1.txt @@ -0,0 +1,65 @@ +Detail for my Mk2_fasterControl_1.ino sketch + * + * (initially released as Mk2_bothDisplays_1 in March 2014) + * This sketch is for diverting suplus PV power to a dump load using a triac or + * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on + * the OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * In this latest version, the pin-allocations have been changed to suit my + * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is + * fed from one of the secondary coils of the transformer. Current is measured + * via Current Transformers at the CT1 and CT1 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. This can be driven in two + * different ways, one with an extra pair of logic chips, and one without. The + * appropriate version of the sketch must be selected by including or commenting + * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code. + * + * September 2014: renamed as Mk2_bothDisplays_2, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - removal of unhelpful comments in the IO pin section; + * - tidier initialisation of display logic in setup(); + * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility); + * + * December 2014: renamed as Mk2_bothDisplays_3, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed) + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances + * + * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed. + * + * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to + * remove a timing uncertainty. + * + * January 2016: updated to Mk2_bothDisplays_3c: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * February 2016: updated to Mk2_bothDisplays_4, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * November 2019: updated to Mk2_fasterControl_1 with these changes: + * - Half way through each mains cycle, a prediction is made of the likely energy level at the + * end of the cycle. That predicted value allows the triac to be switched at the +ve going + * zero-crossing point rather than waiting for a further 10 ms. These changes allow for + * faster switching of the load. + * - The range of the energy bucket has been reduced to one tenth of its former value. This + * allows the unit's operation to commence more rapidly whenever surplus power is available. + * - controlMode is no longer selectable, the unit's operation being effectively hard-coded + * as "Normal" rather than Anti-flicker. + * - Port D3 now supports an indicator which shows when the level in the energy bucket + * reaches either end of its range. While the unit is actively diverting surplus power, + * it is vital that the level in the reduced capacity energy bucket remains within its + * permitted range, hence the addition of this indicator. \ No newline at end of file diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_1a.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_1a.txt new file mode 100644 index 0000000..dc5e558 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_1a.txt @@ -0,0 +1,72 @@ +Detail of my sketch, Mk2_fasterControl_1a.ino + * + * (initially released as Mk2_bothDisplays_1 in March 2014) + * This sketch is for diverting suplus PV power to a dump load using a triac or + * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on + * the OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * In this latest version, the pin-allocations have been changed to suit my + * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is + * fed from one of the secondary coils of the transformer. Current is measured + * via Current Transformers at the CT1 and CT1 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. This can be driven in two + * different ways, one with an extra pair of logic chips, and one without. The + * appropriate version of the sketch must be selected by including or commenting + * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code. + * + * September 2014: renamed as Mk2_bothDisplays_2, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - removal of unhelpful comments in the IO pin section; + * - tidier initialisation of display logic in setup(); + * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility); + * + * December 2014: renamed as Mk2_bothDisplays_3, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed) + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances + * + * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed. + * + * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to + * remove a timing uncertainty. + * + * January 2016: updated to Mk2_bothDisplays_3c: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * February 2016: updated to Mk2_bothDisplays_4, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * January 2020: updated to Mk2_fasterControl_1 with these changes: + * - Half way through each mains cycle, a prediction is made of the likely energy level at the + * end of the cycle. That predicted value allows the triac to be switched at the +ve going + * zero-crossing point rather than waiting for a further 10 ms. These changes allow for + * faster switching of the load. + * - The range of the energy bucket has been reduced to one tenth of its former value. This + * allows the unit's operation to commence more rapidly whenever surplus power is available. + * - controlMode is no longer selectable, the unit's operation being effectively hard-coded + * as "Normal" rather than Anti-flicker. + * - Port D3 now supports an indicator which shows when the level in the energy bucket + * reaches either end of its range. While the unit is actively diverting surplus power, + * it is vital that the level in the reduced capacity energy bucket remains within its + * permitted range, hence the addition of this indicator. + * + * February 2020: updated to Mk2_fasterControl_1a with these changes: + * - removal of some redundant code in the logic for determining the next load state. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_2.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_2.txt new file mode 100644 index 0000000..77a95f6 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_2.txt @@ -0,0 +1,77 @@ +Detail of my sketch, Mk2_fasterControl_2.ino + * + * (initially released as Mk2_bothDisplays_1 in March 2014) + * This sketch is for diverting suplus PV power to a dump load using a triac or + * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on + * the OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * In this latest version, the pin-allocations have been changed to suit my + * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is + * fed from one of the secondary coils of the transformer. Current is measured + * via Current Transformers at the CT1 and CT1 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. This can be driven in two + * different ways, one with an extra pair of logic chips, and one without. The + * appropriate version of the sketch must be selected by including or commenting + * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code. + * + * September 2014: renamed as Mk2_bothDisplays_2, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - removal of unhelpful comments in the IO pin section; + * - tidier initialisation of display logic in setup(); + * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility); + * + * December 2014: renamed as Mk2_bothDisplays_3, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed) + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances + * + * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed. + * + * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to + * remove a timing uncertainty. + * + * January 2016: updated to Mk2_bothDisplays_3c: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * February 2016: updated to Mk2_bothDisplays_4, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * November 2019: updated to Mk2_fasterControl_1 with these changes: + * - Half way through each mains cycle, a prediction is made of the likely energy level at the + * end of the cycle. That predicted value allows the triac to be switched at the +ve going + * zero-crossing point rather than waiting for a further 10 ms. These changes allow for + * faster switching of the load. + * - The range of the energy bucket has been reduced to one tenth of its former value. This + * allows the unit's operation to commence more rapidly whenever surplus power is available. + * - controlMode is no longer selectable, the unit's operation being effectively hard-coded + * as "Normal" rather than Anti-flicker. + * - Port D3 now supports an indicator which shows when the level in the energy bucket + * reaches either end of its range. While the unit is actively diverting surplus power, + * it is vital that the level in the reduced capacity energy bucket remains within its + * permitted range, hence the addition of this indicator. + * + * February 2020: updated to Mk2_fasterControl_1a with these changes: + * - removal of some redundant code in the logic for determining the next load state. + * + * March 2021: updated to Mk2_fasterControl_2 with these changes: + * - extra filtering added to offset the HPF effect of CT1. This allows the energy state in + * 10 ms time to be predicted with more confidence. Specifically, it is no longer necessary + * to include a 30% boost factor after each change of load state. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_3.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_3.txt new file mode 100644 index 0000000..2f7e1e1 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_3.txt @@ -0,0 +1,83 @@ +Detail for my sketch, Mk2_fasterControl_3.ino + +/* Mk2_fasterControl_3.ino + * + * (initially released as Mk2_bothDisplays_1 in March 2014) + * This sketch is for diverting suplus PV power to a dump load using a triac or + * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on + * the OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * In this latest version, the pin-allocations have been changed to suit my + * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is + * fed from one of the secondary coils of the transformer. Current is measured + * via Current Transformers at the CT1 and CT1 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. This can be driven in two + * different ways, one with an extra pair of logic chips, and one without. The + * appropriate version of the sketch must be selected by including or commenting + * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code. + * + * September 2014: renamed as Mk2_bothDisplays_2, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - removal of unhelpful comments in the IO pin section; + * - tidier initialisation of display logic in setup(); + * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility); + * + * December 2014: renamed as Mk2_bothDisplays_3, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed) + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances + * + * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed. + * + * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to + * remove a timing uncertainty. + * + * January 2016: updated to Mk2_bothDisplays_3c: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * February 2016: updated to Mk2_bothDisplays_4, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * November 2019: updated to Mk2_fasterControl_1 with these changes: + * - Half way through each mains cycle, a prediction is made of the likely energy level at the + * end of the cycle. That predicted value allows the triac to be switched at the +ve going + * zero-crossing point rather than waiting for a further 10 ms. These changes allow for + * faster switching of the load. + * - The range of the energy bucket has been reduced to one tenth of its former value. This + * allows the unit's operation to commence more rapidly whenever surplus power is available. + * - controlMode is no longer selectable, the unit's operation being effectively hard-coded + * as "Normal" rather than Anti-flicker. + * - Port D3 now supports an indicator which shows when the level in the energy bucket + * reaches either end of its range. While the unit is actively diverting surplus power, + * it is vital that the level in the reduced capacity energy bucket remains within its + * permitted range, hence the addition of this indicator. + * + * February 2020: updated to Mk2_fasterControl_1a with these changes: + * - removal of some redundant code in the logic for determining the next load state. + * + * March 2021: updated to Mk2_fasterControl_2 with these changes: + * - extra filtering added to offset the HPF effect of CT1. This allows the energy state in + * 10 ms time to be predicted with more confidence. Specifically, it is no longer necessary + * to include a 30% boost factor after each change of load state. + * + * June 2021: updated to Mk2_fasterControl_3 with these changes: + * - to reflect the performance of recently manufactured YHDC SCT_013_000 CTs, + * the value of the parameter lpf_gain has been reduced from 12 to 8. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ \ No newline at end of file diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_RFdatalog_1.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_RFdatalog_1.txt new file mode 100644 index 0000000..aa7eec2 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_RFdatalog_1.txt @@ -0,0 +1,102 @@ +Detail for my sketch, Mk2_fasterControl_RFdatalog_1.ino + +/* Mk2_fasterControl_RFdatalog_1.ino + * + * (initially released as Mk2_bothDisplays_1 in March 2014) + * This sketch is for diverting suplus PV power to a dump load using a triac or + * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on + * the OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * In this latest version, the pin-allocations have been changed to suit my + * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is + * fed from one of the secondary coils of the transformer. Current is measured + * via Current Transformers at the CT1 and CT1 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. With this sketch that supports datalogging + * via RF, the display can only be used with the pin-saving hardware: ICs 3&4. + * + * September 2014: renamed as Mk2_bothDisplays_2, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - removal of unhelpful comments in the IO pin section; + * - tidier initialisation of display logic in setup(); + * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility); + * + * December 2014: renamed as Mk2_bothDisplays_3, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed) + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances + * + * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed. + * + * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to + * remove a timing uncertainty. + * + * January 2016: updated to Mk2_bothDisplays_3c: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * February 2016: updated to Mk2_bothDisplays_4, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * November 2019: updated to Mk2_fasterControl_1 with these changes: + * - Half way through each mains cycle, a prediction is made of the likely energy level at the + * end of the cycle. That predicted value allows the triac to be switched at the +ve going + * zero-crossing point rather than waiting for a further 10 ms. These changes allow for + * faster switching of the load. + * - The range of the energy bucket has been reduced to one tenth of its former value. This + * allows the unit's operation to commence more rapidly whenever surplus power is available. + * - controlMode is no longer selectable, the unit's operation being effectively hard-coded + * as "Normal" rather than Anti-flicker. + * - Port D3 now supports an indicator which shows when the level in the energy bucket + * reaches either end of its range. While the unit is actively diverting surplus power, + * it is vital that the level in the reduced capacity energy bucket remains within its + * permitted range, hence the addition of this indicator. + * + * February 2020: updated to Mk2_fasterControl_twoLoads_1 with these changes: + * - the energy overflow indicator has been disabled to free up port D3 + * - port D3 now supports a second load + * + * February 2020: updated to Mk2_fasterControl_twoLoads_2 with these changes: + * - improved multi-load control logic to prevent the primary load from being disturbed by + * the lower priority one. This logic now mirrors that in the Mk2_multiLoad_wired_n line. + * + * March 2021: updated to Mk2_fasterControl_withRF_1 with these changes: + * - addition of datalogging by RF + * - removal of the option for standard display hardware (which is incompatible with RF) + * + * March 2021: updated to Mk2_fasterControl_2 with these changes: + * - extra filtering added to offset the HPF effect of CT1. This allows the energy state in + * 10 ms time to be predicted with more confidence. Specifically, it is no longer necessary + * to include a 30% boost factor after each change of load state. + * + * June 2021: updated to Mk2_fasterControl_withRF_3 with these changes: + * - to reflect the performance of recently manufactured YHDC SCT_013_000 CTs, + * the value of the parameter lpf_gain has been reduced from 12 to 8. + * + * July 2022: updated to Mk2_fasterControl_withRF_4, with this change: + * - the datalogging accumulators for grid power, diverted power and Vsquared have been rescaled + * to 1/16 of their previous values to avoid the risk of overflowing during a 10-second + * datalogging period. + * + * September 2022: updated to Mk2_fasterControl_RFdatalog_1, with this change: + * - reinstated the code for constraining the energy bucket's level to within its + * working range. This important section of code has unfortunately been missing + * in all versions of the fasterControl_withRF line. The new name should make the + * purpose of this sketch more obvious. + * + * Robin Emley + * + * www.Mk2PVrouter.co.uk + */ diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_twoLoads_1.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_twoLoads_1.txt new file mode 100644 index 0000000..fe449a6 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_twoLoads_1.txt @@ -0,0 +1,75 @@ +Detail for my Mk2_fasterControl_twoLoads_1.ino sketch + + * + * (initially released as Mk2_bothDisplays_1 in March 2014) + * This sketch is for diverting suplus PV power to a dump load using a triac or + * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on + * the OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * In this latest version, the pin-allocations have been changed to suit my + * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is + * fed from one of the secondary coils of the transformer. Current is measured + * via Current Transformers at the CT1 and CT1 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. This can be driven in two + * different ways, one with an extra pair of logic chips, and one without. The + * appropriate version of the sketch must be selected by including or commenting + * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code. + * + * September 2014: renamed as Mk2_bothDisplays_2, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - removal of unhelpful comments in the IO pin section; + * - tidier initialisation of display logic in setup(); + * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility); + * + * December 2014: renamed as Mk2_bothDisplays_3, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed) + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances + * + * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed. + * + * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to + * remove a timing uncertainty. + * + * January 2016: updated to Mk2_bothDisplays_3c: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * February 2016: updated to Mk2_bothDisplays_4, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * November 2019: updated to Mk2_fasterControl_1 with these changes: + * - Half way through each mains cycle, a prediction is made of the likely energy level at the + * end of the cycle. That predicted value allows the triac to be switched at the +ve going + * zero-crossing point rather than waiting for a further 10 ms. These changes allow for + * faster switching of the load. + * - The range of the energy bucket has been reduced to one tenth of its former value. This + * allows the unit's operation to commence more rapidly whenever surplus power is available. + * - controlMode is no longer selectable, the unit's operation being effectively hard-coded + * as "Normal" rather than Anti-flicker. + * - Port D3 now supports an indicator which shows when the level in the energy bucket + * reaches either end of its range. While the unit is actively diverting surplus power, + * it is vital that the level in the reduced capacity energy bucket remains within its + * permitted range, hence the addition of this indicator. + * + * February 20120: updated to Mk2_fasterControl_twoLoads_1 with these changes: + * - the energy overflow indicator has been disabled to free up port D3 + * - port D3 now supports a second load + * + * * Robin Emley + * www.Mk2PVrouter.co.uk + */ + diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_twoLoads_2.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_twoLoads_2.txt new file mode 100644 index 0000000..448e37c --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_twoLoads_2.txt @@ -0,0 +1,79 @@ +Detail for my Mk2_fasterControl_twoLoads_2.ino sketch + + * + * (initially released as Mk2_bothDisplays_1 in March 2014) + * This sketch is for diverting suplus PV power to a dump load using a triac or + * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on + * the OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * In this latest version, the pin-allocations have been changed to suit my + * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is + * fed from one of the secondary coils of the transformer. Current is measured + * via Current Transformers at the CT1 and CT1 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. This can be driven in two + * different ways, one with an extra pair of logic chips, and one without. The + * appropriate version of the sketch must be selected by including or commenting + * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code. + * + * September 2014: renamed as Mk2_bothDisplays_2, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - removal of unhelpful comments in the IO pin section; + * - tidier initialisation of display logic in setup(); + * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility); + * + * December 2014: renamed as Mk2_bothDisplays_3, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed) + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances + * + * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed. + * + * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to + * remove a timing uncertainty. + * + * January 2016: updated to Mk2_bothDisplays_3c: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * February 2016: updated to Mk2_bothDisplays_4, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * November 2019: updated to Mk2_fasterControl_1 with these changes: + * - Half way through each mains cycle, a prediction is made of the likely energy level at the + * end of the cycle. That predicted value allows the triac to be switched at the +ve going + * zero-crossing point rather than waiting for a further 10 ms. These changes allow for + * faster switching of the load. + * - The range of the energy bucket has been reduced to one tenth of its former value. This + * allows the unit's operation to commence more rapidly whenever surplus power is available. + * - controlMode is no longer selectable, the unit's operation being effectively hard-coded + * as "Normal" rather than Anti-flicker. + * - Port D3 now supports an indicator which shows when the level in the energy bucket + * reaches either end of its range. While the unit is actively diverting surplus power, + * it is vital that the level in the reduced capacity energy bucket remains within its + * permitted range, hence the addition of this indicator. + * + * February 20120: updated to Mk2_fasterControl_twoLoads_1 with these changes: + * - the energy overflow indicator has been disabled to free up port D3 + * - port D3 now supports a second load + * + * February 2020: updated to Mk2_fasterControl_twoLoads_2 with these changes: + * - improved multi-load control logic to prevent the primary load from being disturbed by + * the lower priority one. This logic now mirrors that in the Mk2_multiLoad_wired_n line. + * + * * Robin Emley + * www.Mk2PVrouter.co.uk + */ + diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_twoLoads_3.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_twoLoads_3.txt new file mode 100644 index 0000000..1737a1a --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_twoLoads_3.txt @@ -0,0 +1,84 @@ +Detail of my sketch, Mk2_fasterControl_twoLoads_3.ino + * + * (initially released as Mk2_bothDisplays_1 in March 2014) + * This sketch is for diverting suplus PV power to a dump load using a triac or + * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on + * the OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * In this latest version, the pin-allocations have been changed to suit my + * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is + * fed from one of the secondary coils of the transformer. Current is measured + * via Current Transformers at the CT1 and CT1 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. This can be driven in two + * different ways, one with an extra pair of logic chips, and one without. The + * appropriate version of the sketch must be selected by including or commenting + * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code. + * + * September 2014: renamed as Mk2_bothDisplays_2, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - removal of unhelpful comments in the IO pin section; + * - tidier initialisation of display logic in setup(); + * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility); + * + * December 2014: renamed as Mk2_bothDisplays_3, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed) + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances + * + * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed. + * + * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to + * remove a timing uncertainty. + * + * January 2016: updated to Mk2_bothDisplays_3c: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * February 2016: updated to Mk2_bothDisplays_4, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * November 2019: updated to Mk2_fasterControl_1 with these changes: + * - Half way through each mains cycle, a prediction is made of the likely energy level at the + * end of the cycle. That predicted value allows the triac to be switched at the +ve going + * zero-crossing point rather than waiting for a further 10 ms. These changes allow for + * faster switching of the load. + * - The range of the energy bucket has been reduced to one tenth of its former value. This + * allows the unit's operation to commence more rapidly whenever surplus power is available. + * - controlMode is no longer selectable, the unit's operation being effectively hard-coded + * as "Normal" rather than Anti-flicker. + * - Port D3 now supports an indicator which shows when the level in the energy bucket + * reaches either end of its range. While the unit is actively diverting surplus power, + * it is vital that the level in the reduced capacity energy bucket remains within its + * permitted range, hence the addition of this indicator. + * + * February 2020: updated to Mk2_fasterControl_twoLoads_1 with these changes: + * - the energy overflow indicator has been disabled to free up port D3 + * - port D3 now supports a second load + * + * February 2020: updated to Mk2_fasterControl_twoLoads_2 with these changes: + * - improved multi-load control logic to prevent the primary load from being disturbed by + * the lower priority one. This logic now mirrors that in the Mk2_multiLoad_wired_n line. + * + * March 2021: updated to Mk2_fasterControl_twoLoads_3 with these changes: + * - extra filtering added to offset the HPF effect of CT1. This allows the energy state in + * 10 ms time to be predicted with more confidence. Specifically, it is no longer necessary + * to include a 30% boost factor after each change of load state. + * + * + * * Robin Emley + * www.Mk2PVrouter.co.uk + */ + diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_twoLoads_4.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_twoLoads_4.txt new file mode 100644 index 0000000..323398a --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_twoLoads_4.txt @@ -0,0 +1,89 @@ +Detail for my sketch, Mk2_fasterControl_twoLoads_4.ino + +/* Mk2_fasterControl_twoLoads_4.ino + * + * (initially released as Mk2_bothDisplays_1 in March 2014) + * This sketch is for diverting suplus PV power to a dump load using a triac or + * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on + * the OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * In this latest version, the pin-allocations have been changed to suit my + * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is + * fed from one of the secondary coils of the transformer. Current is measured + * via Current Transformers at the CT1 and CT1 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. This can be driven in two + * different ways, one with an extra pair of logic chips, and one without. The + * appropriate version of the sketch must be selected by including or commenting + * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code. + * + * September 2014: renamed as Mk2_bothDisplays_2, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - removal of unhelpful comments in the IO pin section; + * - tidier initialisation of display logic in setup(); + * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility); + * + * December 2014: renamed as Mk2_bothDisplays_3, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed) + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances + * + * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed. + * + * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to + * remove a timing uncertainty. + * + * January 2016: updated to Mk2_bothDisplays_3c: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * February 2016: updated to Mk2_bothDisplays_4, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * November 2019: updated to Mk2_fasterControl_1 with these changes: + * - Half way through each mains cycle, a prediction is made of the likely energy level at the + * end of the cycle. That predicted value allows the triac to be switched at the +ve going + * zero-crossing point rather than waiting for a further 10 ms. These changes allow for + * faster switching of the load. + * - The range of the energy bucket has been reduced to one tenth of its former value. This + * allows the unit's operation to commence more rapidly whenever surplus power is available. + * - controlMode is no longer selectable, the unit's operation being effectively hard-coded + * as "Normal" rather than Anti-flicker. + * - Port D3 now supports an indicator which shows when the level in the energy bucket + * reaches either end of its range. While the unit is actively diverting surplus power, + * it is vital that the level in the reduced capacity energy bucket remains within its + * permitted range, hence the addition of this indicator. + * + * February 2020: updated to Mk2_fasterControl_twoLoads_1 with these changes: + * - the energy overflow indicator has been disabled to free up port D3 + * - port D3 now supports a second load + * + * February 2020: updated to Mk2_fasterControl_twoLoads_2 with these changes: + * - improved multi-load control logic to prevent the primary load from being disturbed by + * the lower priority one. This logic now mirrors that in the Mk2_multiLoad_wired_n line. + * + * March 2021: updated to Mk2_fasterControl_twoLoads_3 with these changes: + * - extra filtering added to offset the HPF effect of CT1. This allows the energy state in + * 10 ms time to be predicted with more confidence. Specifically, it is no longer necessary + * to include a 30% boost factor after each change of load state. + * + * June 2021: updated to Mk2_fasterControl_twoLoads_4 with these changes: + * - to reflect the performance of recently manufactured YHDC SCT_013_000 CTs, + * the value of the parameter lpf_gain has been reduced from 12 to 8. + * + * + * * Robin Emley + * www.Mk2PVrouter.co.uk + */ \ No newline at end of file diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_twoLoads_5.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_twoLoads_5.txt new file mode 100644 index 0000000..ff25fdc --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_twoLoads_5.txt @@ -0,0 +1,90 @@ +Detail for my sketch, Mk2_fasterControl_twoLoads_5.ino + +/* Mk2_fasterControl_twoLoads_5.ino + * + * (initially released as Mk2_bothDisplays_1 in March 2014) + * This sketch is for diverting suplus PV power to a dump load using a triac or + * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on + * the OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * In this latest version, the pin-allocations have been changed to suit my + * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is + * fed from one of the secondary coils of the transformer. Current is measured + * via Current Transformers at the CT1 and CT1 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. This can be driven in two + * different ways, one with an extra pair of logic chips, and one without. The + * appropriate version of the sketch must be selected by including or commenting + * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code. + * + * September 2014: renamed as Mk2_bothDisplays_2, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - removal of unhelpful comments in the IO pin section; + * - tidier initialisation of display logic in setup(); + * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility); + * + * December 2014: renamed as Mk2_bothDisplays_3, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed) + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances + * + * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed. + * + * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to + * remove a timing uncertainty. + * + * January 2016: updated to Mk2_bothDisplays_3c: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * February 2016: updated to Mk2_bothDisplays_4, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * November 2019: updated to Mk2_fasterControl_1 with these changes: + * - Half way through each mains cycle, a prediction is made of the likely energy level at the + * end of the cycle. That predicted value allows the triac to be switched at the +ve going + * zero-crossing point rather than waiting for a further 10 ms. These changes allow for + * faster switching of the load. + * - The range of the energy bucket has been reduced to one tenth of its former value. This + * allows the unit's operation to commence more rapidly whenever surplus power is available. + * - controlMode is no longer selectable, the unit's operation being effectively hard-coded + * as "Normal" rather than Anti-flicker. + * - Port D3 now supports an indicator which shows when the level in the energy bucket + * reaches either end of its range. While the unit is actively diverting surplus power, + * it is vital that the level in the reduced capacity energy bucket remains within its + * permitted range, hence the addition of this indicator. + * + * February 2020: updated to Mk2_fasterControl_1a with these changes: + * - removal of some redundant code in the logic for determining the next load state. + * + * March 2021: updated to Mk2_fasterControl_2 with these changes: + * - extra filtering added to offset the HPF effect of CT1. This allows the energy state in + * 10 ms time to be predicted with more confidence. Specifically, it is no longer necessary + * to include a 30% boost factor after each change of load state. + * + * June 2021: updated to Mk2_fasterControl_3 with these changes: + * - to reflect the performance of recently manufactured YHDC SCT_013_000 CTs, + * the value of the parameter lpf_gain has been reduced from 12 to 8. + * + * July 2023: updated to Mk2_fasterControl_twoLoads_5 with these changes: + * - the ability to control two loads has been transferred from the latest version of my + * standard multiLoad sketch, Mk2_multiLoad_wired_7a. The faster control algorithm + * has been retained. + * The previous 2-load "faster control" sketch (version 4) has been archived as its + * behaviour was found to be problematic. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_withRemoteLoad_1.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_withRemoteLoad_1.txt new file mode 100644 index 0000000..57ade4f --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_fasterControl_withRemoteLoad_1.txt @@ -0,0 +1,103 @@ +Detail for my sketch Mk2_fasterControl_withRemoteLoad_1.ino + +/* Mk2_fasterControl_withRemoteLoad_1.ino + * + * (initially released as Mk2_bothDisplays_1 in March 2014) + * This sketch is for diverting suplus PV power to a dump load using a triac or + * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on + * the OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * In this latest version, the pin-allocations have been changed to suit my + * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is + * fed from one of the secondary coils of the transformer. Current is measured + * via Current Transformers at the CT1 and CT1 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. With this sketch that supports a remote load + * that is controlled via RF, the display can only be used with the pin-saving hardware: ICs 3&4. + * + * September 2014: renamed as Mk2_bothDisplays_2, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - removal of unhelpful comments in the IO pin section; + * - tidier initialisation of display logic in setup(); + * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility); + * + * December 2014: renamed as Mk2_bothDisplays_3, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed) + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances + * + * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed. + * + * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to + * remove a timing uncertainty. + * + * January 2016: updated to Mk2_bothDisplays_3c: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * February 2016: updated to Mk2_bothDisplays_4, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * November 2019: updated to Mk2_fasterControl_1 with these changes: + * - Half way through each mains cycle, a prediction is made of the likely energy level at the + * end of the cycle. That predicted value allows the triac to be switched at the +ve going + * zero-crossing point rather than waiting for a further 10 ms. These changes allow for + * faster switching of the load. + * - The range of the energy bucket has been reduced to one tenth of its former value. This + * allows the unit's operation to commence more rapidly whenever surplus power is available. + * - controlMode is no longer selectable, the unit's operation being effectively hard-coded + * as "Normal" rather than Anti-flicker. + * - Port D3 now supports an indicator which shows when the level in the energy bucket + * reaches either end of its range. While the unit is actively diverting surplus power, + * it is vital that the level in the reduced capacity energy bucket remains within its + * permitted range, hence the addition of this indicator. + * + * February 2020: updated to Mk2_fasterControl_twoLoads_1 with these changes: + * - the energy overflow indicator has been disabled to free up port D3 + * - port D3 now supports a second load + * + * February 2020: updated to Mk2_fasterControl_twoLoads_2 with these changes: + * - improved multi-load control logic to prevent the primary load from being disturbed by + * the lower priority one. This logic now mirrors that in the Mk2_multiLoad_wired_n line. + * + * March 2021: updated to Mk2_fasterControl_withRF_1 with these changes: + * - addition of datalogging by RF + * - removal of the option for standard display hardware (which is incompatible with RF) + * + * March 2021: updated to Mk2_fasterControl_2 with these changes: + * - extra filtering added to offset the HPF effect of CT1. This allows the energy state in + * 10 ms time to be predicted with more confidence. Specifically, it is no longer necessary + * to include a 30% boost factor after each change of load state. + * + * June 2021: updated to Mk2_fasterControl_withRF_3 with these changes: + * - to reflect the performance of recently manufactured YHDC SCT_013_000 CTs, + * the value of the parameter lpf_gain has been reduced from 12 to 8. + * + * July 2022: updated to Mk2_fasterControl_withRF_4, with this change: + * - the datalogging accumulators for grid power, diverted power and Vsquared have been rescaled + * to 1/16 of their previous values to avoid the risk of overflowing during a 10-second + * datalogging period. + * + * September 2022: updated to Mk2_fasterControl_withRemoteLoad_1 with these changes: + * - remove all code for datalogging + * - add code to support a remote load via RF control. A one-integer on/off instruction can be sent every + * mains cycle with a refresh message being sent every 5 mains cycles if the required state of + * the load has not changed. For use with the receiver sketch, remoteUnit_fasterControl_n. + * - increase the hardware timer duration from 125 us to 150 us (just to reduce the workload) + * + * Robin Emley + * + * www.Mk2PVrouter.co.uk + */ diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_CAT5_1.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_CAT5_1.txt new file mode 100644 index 0000000..d796896 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_CAT5_1.txt @@ -0,0 +1,27 @@ +Detail for my Mk2_multiLoad_CAT5_1.ino sketch + +This is an upgraded version of the original Mk2 sketch for use with my +PCB-based hardware. It still supports two current sensors, CT1 and CT2. +CT1 is for monitoring the flow of energy at the supply point; CT2 is +available for monitoring the flow of current to the primary dump-load. + +With this version, multiple dump-loads are supported, each being +controlled by its own dedicated IO pin. To generate sufficient spare +IO pins for this purpose, the RF facility has been removed from this code. +By using the pin-saving hardware option, the 4-digit display is still +available for use. + +The primary load (Load 0) is still controlled from the "trigger" port. The +five IO drivers that have been freed up by not using the RF module have been +re-allocated to control five additional loads. These signals can be +conveniently accessed at the left-hand side of the J1-5 connector on the PCB. +The uppermost pin is for Load 1, the lowermost one is for Load 6. + +By mistake, I posted this code with modified logic to provide active-high +control signals. This arrangement would be more appropriate for use with +SSRs rather than with the Motorola MOC3041 which is normally use to drive +a BTA41 triac. Rather than removing it entirely, this active-high version +has been relegated to the Archive section. The equivalent active-low version, +which will is of more relevance to the hardware that is available from this +website, has been posted as "_2". + diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_CAT5_2.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_CAT5_2.txt new file mode 100644 index 0000000..f10d86c --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_CAT5_2.txt @@ -0,0 +1,22 @@ +Detail for my Mk2_multiLoad_CAT5_2.ino sketch + +This is an upgraded version of the original Mk2 sketch for use with my +PCB-based hardware. It still supports two current sensors, CT1 and CT2. +CT1 is for monitoring the flow of energy at the supply point; CT2 is +available for monitoring the flow of current to the primary dump-load. + +With this version, multiple dump-loads are supported, each being +controlled by its own dedicated IO pin. To generate sufficient spare +IO pins for this purpose, the RF facility has been removed from this code. +By using the pin-saving hardware option, the 4-digit display is still +available for use. + +The primary load (Load 0) is still controlled from the "trigger" port. The +five IO drivers that have been freed up by not using the RF module have been +re-allocated to control five additional loads. These signals can be +conveniently accessed at the left-hand side of the J1-5 connector on the PCB. +The uppermost pin is for Load 1, the lowermost one is for Load 6. + +For compatibility with previous versions, all loads are driven using +active-low logic. + diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_CAT5_3.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_CAT5_3.txt new file mode 100644 index 0000000..e7e7577 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_CAT5_3.txt @@ -0,0 +1,28 @@ +Detail for my Mk2_multiLoad_CAT5_3.ino sketch + +This is an upgraded version of the original Mk2 sketch for use with my +PCB-based hardware. It still supports two current sensors, CT1 and CT2. +CT1 is for monitoring the flow of energy at the supply point; CT2 is +available for monitoring the flow of current to the primary dump-load. + +With this version, multiple dump-loads are supported, each being +controlled by its own dedicated IO pin. To generate sufficient spare +IO pins for this purpose, the RF facility has been removed from this code. +By using the pin-saving hardware option, the 4-digit display is still +available for use. + +The primary load (Load 0) is still controlled from the "trigger" port. The +five IO drivers that have been freed up by not using the RF module have been +re-allocated to control five additional loads. These signals can be +conveniently accessed at the left-hand side of the J1-5 connector on the PCB. +The uppermost pin is for Load 1, the lowermost one is for Load 6. + +For compatibility with previous versions, all loads are driven using +active-low logic. + +Changes for version _3: + +- The 'long' variable, cycleCount, which counted mains cycles since start-up, has + been removed. This variable would have eventually overflowed which could have + caused unpredictable effects. The related functionality has been re-implemented + using individual 'int' counters. \ No newline at end of file diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_CAT5_4.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_CAT5_4.txt new file mode 100644 index 0000000..5ec6923 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_CAT5_4.txt @@ -0,0 +1,42 @@ +Detail for my Mk2_multiLoad_CAT5_4.ino sketch + +This is an upgraded version of the original Mk2 sketch for use with my +PCB-based hardware. It still supports two current sensors, CT1 and CT2. +CT1 is for monitoring the flow of energy at the supply point; CT2 is +available for monitoring the flow of current to the primary dump-load. + +With this version, multiple dump-loads are supported, each being +controlled by its own dedicated IO pin. To generate sufficient spare +IO pins for this purpose, the RF facility has been removed from this code. +By using the pin-saving hardware option, the 4-digit display is still +available for use. + +The primary load (Load 0) is still controlled from the "trigger" port. The +five IO drivers that have been freed up by not using the RF module have been +re-allocated to control five additional loads. These signals can be accessed +at the left-hand side of the J1-5 connector on the PCB. The uppermost pin is +for Load 1; the lowermost one is for Load 6. + +For versions 1 to 3, all loads are driven using active-low logic. +For version 4, the additional loads (Nos 1 - 5) use active-high logic. When using the +green PCB, these points can all be accessed alongside a 0V pin (with 0.2" spacing). + +Changes for version _3: + +- The 'long' variable, cycleCount, which counted mains cycles since start-up, has + been removed. This variable would have eventually overflowed which could have + caused unpredictable effects. The related functionality has been re-implemented + using individual 'int' counters. + +Changes for version _4: + +- a persistence check for the zero-crossing detection has been added. This +is to remove any false detections of zero-crossings. This effect is seen more +with some types of transformer than others. + +- a mechanism has been added to monitor and display the minimum number +of sample sets that occur each mains cycle. With a 125us timebase, and three +ADC samples per set, the expected number of sample sets per 20ms mains cycle +is 20 / (3 * 0.125) = 53.33. Any value less than 53 would indicate a loss of data. + +- The 5 additional loads are now driven active-high rather than active-low. diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_5.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_5.txt new file mode 100644 index 0000000..8720404 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_5.txt @@ -0,0 +1,55 @@ +Detail for my Mk2_multiLoad_wired_5.ino sketch + +This is an upgraded version of the original Mk2 sketch for use with my +PCB-based hardware. It still supports two current sensors, CT1 and CT2. +CT1 is for monitoring the flow of energy at the supply point; CT2 is +available for monitoring the flow of current to the primary dump-load. + +With this version, multiple dump-loads are supported, each being +controlled by its own dedicated IO pin. To generate sufficient spare +IO pins for this purpose, the RF facility has been removed from this code. +By using the pin-saving hardware option, the 4-digit display is still +available for use. + +The primary load (Load 0) is still controlled from the "trigger" port. The +five IO drivers that have been freed up by not using the RF module have been +re-allocated to control five additional loads. These signals can be accessed +at the left-hand side of the J1-5 connector on the PCB. The uppermost pin is +for Load 1; the lowermost one is for Load 6. + +For versions 1 to 3, all loads are driven using active-low logic. +For version 4, the additional loads (Nos 1 - 5) use active-high logic. When +using my green PCB, each of these points can also be accessed alongside a 0V pin +(with 0.2" spacing). The main(green) board page at www.mk2pvrouter.co.uk has +details. + +Changes for version _3: + +- The 'long' variable, cycleCount, which counted mains cycles since start-up, has + been removed. This variable would have eventually overflowed which could have + caused unpredictable effects. The related functionality has been re-implemented + using individual 'int' counters. + +Changes for version _4: + +- a persistence check for the zero-crossing detection has been added. This +is to remove any false detections of zero-crossings. This effect is seen more +with some types of transformer than others. + +- a mechanism has been added to monitor and display the minimum number +of sample sets that occur each mains cycle. With a 125us timebase, and three +ADC samples per set, the expected number of sample sets per 20ms mains cycle +is 20 / (3 * 0.125) = 53.33. Any value less than 53 would indicate a loss of data. + +- The 5 additional loads are now driven active-high rather than active-low. + +Changes for version _5: + +- change of name from Mk2_multiLoad_CAT5_n to Mk2_multiLoad_wired_n +- the original twin-threshold algorithm for energy state management has been reinstated; +- improved mechanism for controlling multiple loads (faster and more accurate); +- the phaseCal mechanism has been reinstated; +- SWEETZONE_IN_JOULES has been replaced by WORKING_RANGE_IN_JOULES. + + + diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_5a.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_5a.txt new file mode 100644 index 0000000..209e7f2 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_5a.txt @@ -0,0 +1,61 @@ +Detail for my Mk2_multiLoad_wired_5a.ino sketch + +This is an upgraded version of the original Mk2 sketch for use with my +PCB-based hardware. It still supports two current sensors, CT1 and CT2. +CT1 is for monitoring the flow of energy at the supply point; CT2 is +available for monitoring the flow of current to the primary dump-load. + +With this version, multiple dump-loads are supported, each being +controlled by its own dedicated IO pin. To generate sufficient spare +IO pins for this purpose, the RF facility has been removed from this code. +By using the pin-saving hardware option, the 4-digit display is still +available for use. + +The primary load (Load 0) is still controlled from the "trigger" port. The +five IO drivers that have been freed up by not using the RF module have been +re-allocated to control five additional loads. These signals can be accessed +at the left-hand side of the J1-5 connector on the PCB. The uppermost pin is +for Load 1; the lowermost one is for Load 6. + +For versions 1 to 3, all loads are driven using active-low logic. +For version 4, the additional loads (Nos 1 - 5) use active-high logic. When +using my green PCB, each of these points can also be accessed alongside a 0V pin +(with 0.2" spacing). The main(green) board page at www.mk2pvrouter.co.uk has +details. + +Changes for version _3: + +- The 'long' variable, cycleCount, which counted mains cycles since start-up, has + been removed. This variable would have eventually overflowed which could have + caused unpredictable effects. The related functionality has been re-implemented + using individual 'int' counters. + +Changes for version _4: + +- a persistence check for the zero-crossing detection has been added. This +is to remove any false detections of zero-crossings. This effect is seen more +with some types of transformer than others. + +- a mechanism has been added to monitor and display the minimum number +of sample sets that occur each mains cycle. With a 125us timebase, and three +ADC samples per set, the expected number of sample sets per 20ms mains cycle +is 20 / (3 * 0.125) = 53.33. Any value less than 53 would indicate a loss of data. + +- The 5 additional loads are now driven active-high rather than active-low. + +Changes for version _5: + +- change of name from Mk2_multiLoad_CAT5_n to Mk2_multiLoad_wired_n +- the original twin-threshold algorithm for energy state management has been reinstated; +- improved mechanism for controlling multiple loads (faster and more accurate); +- the phaseCal mechanism has been reinstated; +- SWEETZONE_IN_JOULES has been replaced by WORKING_RANGE_IN_JOULES. + +Changes for version _5a: + +- a minor bug-fix in allGeneralProcessing() re. the way that the energy thresholds are + adjusted in the period immediatetly after a change of load state has occurred. + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_6.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_6.txt new file mode 100644 index 0000000..9306ca7 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_6.txt @@ -0,0 +1,68 @@ +Detail for my Mk2_multiLoad_wired_6.ino sketch + +This is an upgraded version of the original Mk2 sketch for use with my +PCB-based hardware. It still supports two current sensors, CT1 and CT2. +CT1 is for monitoring the flow of energy at the supply point; CT2 is +available for monitoring the flow of current to the primary dump-load. + +With this version, multiple dump-loads are supported, each being +controlled by its own dedicated IO pin. To generate sufficient spare +IO pins for this purpose, the RF facility has been removed from this code. +By using the pin-saving hardware option, the 4-digit display is still +available for use. + +The primary load (Load 0) is still controlled from the "trigger" port. The +five IO drivers that have been freed up by not using the RF module have been +re-allocated to control five additional loads. These signals can be accessed +at the left-hand side of the J1-5 connector on the PCB. The uppermost pin is +for Load 1; the lowermost one is for Load 6. + +For versions 1 to 3, all loads are driven using active-low logic. +For version 4, the additional loads (Nos 1 - 5) use active-high logic. When +using my green PCBs, each of these points can be accessed alongside a 0V pin. +The main PCB page at www.mk2pvrouter.co.uk has details for the access points +for each of the IO ports. + +Changes for version _3: + +- The 'long' variable, cycleCount, which counted mains cycles since start-up, has + been removed. This variable would have eventually overflowed which could have + caused unpredictable effects. The related functionality has been re-implemented + using individual 'int' counters. + +Changes for version _4: + +- a persistence check for the zero-crossing detection has been added. This +is to remove any false detections of zero-crossings. This effect is seen more +with some types of transformer than others. + +- a mechanism has been added to monitor and display the minimum number +of sample sets that occur each mains cycle. With a 125us timebase, and three +ADC samples per set, the expected number of sample sets per 20ms mains cycle +is 20 / (3 * 0.125) = 53.33. Any value less than 53 would indicate a loss of data. + +- The 5 additional loads are now driven active-high rather than active-low. + +Changes for version _5: (not to be used) + +- change of name from Mk2_multiLoad_CAT5_n to Mk2_multiLoad_wired_n +- the original twin-threshold algorithm for energy state management has been reinstated; +- improved mechanism for controlling multiple loads (faster and more accurate); +- the phaseCal mechanism has been reinstated; +- SWEETZONE_IN_JOULES has been replaced by WORKING_RANGE_IN_JOULES. + +Changes for version _5a: (not to be used) + +- a minor bug-fix in allGeneralProcessing() re. the way that the energy thresholds are + adjusted in the period immediatetly after a change of load state has occurred. + +Changes for version _6: + +- Minimum and maximum limits for the energy bucket's level have been reinstated. This + section of code had become lost during the upgrade from version 4 to version 5. + Without this code in place, neither of the version 5 relesases will operate as + intended, so should not be used. Version 6 is believed to be a correct implementation + of the improved mechanism for controlling multiple loads. + + + diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_6a.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_6a.txt new file mode 100644 index 0000000..799dae8 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_6a.txt @@ -0,0 +1,75 @@ +Detail for my Mk2_multiLoad_wired_6a.ino sketch + +This is an upgraded version of the original Mk2 sketch for use with my +PCB-based hardware. It still supports two current sensors, CT1 and CT2. +CT1 is for monitoring the flow of energy at the supply point; CT2 is +available for monitoring the flow of current to the primary dump-load. + +With this version, multiple dump-loads are supported, each being +controlled by its own dedicated IO pin. To generate sufficient spare +IO pins for this purpose, the RF facility has been removed from this code. +By using the pin-saving hardware option, the 4-digit display is still +available for use. + +The primary load (Load 0) is still controlled from the "trigger" port. The +five IO drivers that have been freed up by not using the RF module have been +re-allocated to control five additional loads. These signals can be accessed +at the left-hand side of the J1-5 connector on the PCB. The uppermost pin is +for Load 1; the lowermost one is for Load 6. + +For versions 1 to 3, all loads are driven using active-low logic. +For version 4, the additional loads (Nos 1 - 5) use active-high logic. When +using my green PCBs, each of these points can be accessed alongside a 0V pin. +The main PCB page at www.mk2pvrouter.co.uk has details for the access points +for each of the IO ports. + +Changes for version _3: + +- The 'long' variable, cycleCount, which counted mains cycles since start-up, has + been removed. This variable would have eventually overflowed which could have + caused unpredictable effects. The related functionality has been re-implemented + using individual 'int' counters. + +Changes for version _4: + +- a persistence check for the zero-crossing detection has been added. This +is to remove any false detections of zero-crossings. This effect is seen more +with some types of transformer than others. + +- a mechanism has been added to monitor and display the minimum number +of sample sets that occur each mains cycle. With a 125us timebase, and three +ADC samples per set, the expected number of sample sets per 20ms mains cycle +is 20 / (3 * 0.125) = 53.33. Any value less than 53 would indicate a loss of data. + +- The 5 additional loads are now driven active-high rather than active-low. + +Changes for version _5: (not to be used) + +- change of name from Mk2_multiLoad_CAT5_n to Mk2_multiLoad_wired_n +- the original twin-threshold algorithm for energy state management has been reinstated; +- improved mechanism for controlling multiple loads (faster and more accurate); +- the phaseCal mechanism has been reinstated; +- SWEETZONE_IN_JOULES has been replaced by WORKING_RANGE_IN_JOULES. + +Changes for version _5a: (not to be used) + +- a minor bug-fix in allGeneralProcessing() re. the way that the energy thresholds are + adjusted in the period immediatetly after a change of load state has occurred. + +Changes for version _6: + +- Minimum and maximum limits for the energy bucket's level have been reinstated. This + section of code had become lost during the upgrade from version 4 to version 5. + Without this code in place, neither of the version 5 relesases will operate as + intended, so should not be used. Version 6 is believed to be a correct implementation + of the improved mechanism for controlling multiple loads. + +Changes for version _6a: + +- A minor change has been made to the function timerIsr() so as to resolve +a timing anomaly that has previously existed. With the new arrangement, a +complete set of data samples is made available by the ISR for use by the +main code. There is no longer any possibility of these values being +overwritten before they are processed. + +- the display timeout period has been reduced to 8 hours instead of 10. diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_6b.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_6b.txt new file mode 100644 index 0000000..f96c5ac --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_6b.txt @@ -0,0 +1,81 @@ +Detail for my Mk2_multiLoad_wired_6b.ino sketch + +This is an upgraded version of the original Mk2 sketch for use with my +PCB-based hardware. It still supports two current sensors, CT1 and CT2. +CT1 is for monitoring the flow of energy at the supply point; CT2 is +available for monitoring the flow of current to the primary dump-load. + +With this version, multiple dump-loads are supported, each being +controlled by its own dedicated IO pin. To generate sufficient spare +IO pins for this purpose, the RF facility has been removed from this code. +By using the pin-saving hardware option, the 4-digit display is still +available for use. + +The primary load (Load 0) is still controlled from the "trigger" port. The +five IO drivers that have been freed up by not using the RF module have been +re-allocated to control five additional loads. These signals can be accessed +at the left-hand side of the J1-5 connector on the PCB. The uppermost pin is +for Load 1; the lowermost one is for Load 6. + +For versions 1 to 3, all loads are driven using active-low logic. +For version 4, the additional loads (Nos 1 - 5) use active-high logic. When +using my green PCBs, each of these points can be accessed alongside a 0V pin. +The main PCB page at www.mk2pvrouter.co.uk has details for the access points +for each of the IO ports. + +Changes for version _3: + +- The 'long' variable, cycleCount, which counted mains cycles since start-up, has + been removed. This variable would have eventually overflowed which could have + caused unpredictable effects. The related functionality has been re-implemented + using individual 'int' counters. + +Changes for version _4: + +- a persistence check for the zero-crossing detection has been added. This +is to remove any false detections of zero-crossings. This effect is seen more +with some types of transformer than others. + +- a mechanism has been added to monitor and display the minimum number +of sample sets that occur each mains cycle. With a 125us timebase, and three +ADC samples per set, the expected number of sample sets per 20ms mains cycle +is 20 / (3 * 0.125) = 53.33. Any value less than 53 would indicate a loss of data. + +- The 5 additional loads are now driven active-high rather than active-low. + +Changes for version _5: (not to be used) + +- change of name from Mk2_multiLoad_CAT5_n to Mk2_multiLoad_wired_n +- the original twin-threshold algorithm for energy state management has been reinstated; +- improved mechanism for controlling multiple loads (faster and more accurate); +- the phaseCal mechanism has been reinstated; +- SWEETZONE_IN_JOULES has been replaced by WORKING_RANGE_IN_JOULES. + +Changes for version _5a: (not to be used) + +- a minor bug-fix in allGeneralProcessing() re. the way that the energy thresholds are + adjusted in the period immediatetly after a change of load state has occurred. + +Changes for version _6: + +- Minimum and maximum limits for the energy bucket's level have been reinstated. This + section of code had become lost during the upgrade from version 4 to version 5. + Without this code in place, neither of the version 5 relesases will operate as + intended, so should not be used. Version 6 is believed to be a correct implementation + of the improved mechanism for controlling multiple loads. + +Changes for version _6a: + +- A minor change has been made to the function timerIsr() so as to resolve +a timing anomaly that has previously existed. With the new arrangement, a +complete set of data samples is made available by the ISR for use by the +main code. There is no longer any possibility of these values being +overwritten before they are processed. + +- the display timeout period has been reduced to 8 hours instead of 10. + +Changes for version _6b: +- The variables to store ADC data are now declared as "volatile" to remove +any possibility of incorrect operation due to optimisation by the compiler. + + diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_7.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_7.txt new file mode 100644 index 0000000..1e38c41 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_7.txt @@ -0,0 +1,90 @@ +Detail for my Mk2_multiLoad_wired_7.ino sketch + +This is an upgraded version of the original Mk2 sketch for use with my +PCB-based hardware. It still supports two current sensors, CT1 and CT2. +CT1 is for monitoring the flow of energy at the supply point; CT2 is +available for monitoring the flow of current to the primary dump-load. + +With this version, multiple dump-loads are supported, each being +controlled by its own dedicated IO pin. To generate sufficient spare +IO pins for this purpose, the RF facility has been removed from this code. +By using the pin-saving hardware option, the 4-digit display is still +available for use. + +The primary load (Load 0) is still controlled from the "trigger" port. The +five IO drivers that have been freed up by not using the RF module have been +re-allocated to control five additional loads. These signals can be accessed +at the left-hand side of the J1-5 connector on the PCB. The uppermost pin is +for Load 1; the lowermost one is for Load 6. + +For versions 1 to 3, all loads are driven using active-low logic. +For version 4, the additional loads (Nos 1 - 5) use active-high logic. When +using my green PCBs, each of these points can be accessed alongside a 0V pin. +The main PCB page at www.mk2pvrouter.co.uk has details for the access points +for each of the IO ports. + +Changes for version _3: + +- The 'long' variable, cycleCount, which counted mains cycles since start-up, has + been removed. This variable would have eventually overflowed which could have + caused unpredictable effects. The related functionality has been re-implemented + using individual 'int' counters. + +Changes for version _4: + +- a persistence check for the zero-crossing detection has been added. This +is to remove any false detections of zero-crossings. This effect is seen more +with some types of transformer than others. + +- a mechanism has been added to monitor and display the minimum number +of sample sets that occur each mains cycle. With a 125us timebase, and three +ADC samples per set, the expected number of sample sets per 20ms mains cycle +is 20 / (3 * 0.125) = 53.33. Any value less than 53 would indicate a loss of data. + +- The 5 additional loads are now driven active-high rather than active-low. + +Changes for version _5: (not to be used) + +- change of name from Mk2_multiLoad_CAT5_n to Mk2_multiLoad_wired_n +- the original twin-threshold algorithm for energy state management has been reinstated; +- improved mechanism for controlling multiple loads (faster and more accurate); +- the phaseCal mechanism has been reinstated; +- SWEETZONE_IN_JOULES has been replaced by WORKING_RANGE_IN_JOULES. + +Changes for version _5a: (not to be used) + +- a minor bug-fix in allGeneralProcessing() re. the way that the energy thresholds are + adjusted in the period immediatetly after a change of load state has occurred. + +Changes for version _6: + +- Minimum and maximum limits for the energy bucket's level have been reinstated. This + section of code had become lost during the upgrade from version 4 to version 5. + Without this code in place, neither of the version 5 relesases will operate as + intended, so should not be used. Version 6 is believed to be a correct implementation + of the improved mechanism for controlling multiple loads. + +Changes for version _6a: + +- A minor change has been made to the function timerIsr() so as to resolve +a timing anomaly that has previously existed. With the new arrangement, a +complete set of data samples is made available by the ISR for use by the +main code. There is no longer any possibility of these values being +overwritten before they are processed. + +- the display timeout period has been reduced to 8 hours instead of 10. + +Changes for version _6b: +- The variables to store ADC data are now declared as "volatile" to remove +any possibility of incorrect operation due to optimisation by the compiler. + +Changes for version _7: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + + diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_7a.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_7a.txt new file mode 100644 index 0000000..7425898 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_multiLoad_wired_7a.txt @@ -0,0 +1,81 @@ +Detail for my sketch, Mk2_multiLoad_wired_7a.ino + * + * This sketch is for diverting suplus PV power using multiple hard-wired loads. + * An external switch allows either load 0 or Load 1 to have the highest + * priority. Any number of loads can be supported by the logic, a dedicated + * IO pin being required for each one. + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The selector switch, as mentioned above, connects to the "mode" port which has been + * re-assigned for priority selection. "Normal" mode can be achieved by setting the + * anti-flicker offset prameter to zero at compile-time. + * + * The integral voltage sensor is fed from one of the secondary coils of the transformer. + * Current is measured via Current Transformers at the CT1 and CT1 ports. + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the 'diverted' current, so that energy which is diverted via the primary + * dump-load can be recorded and displayed locally. + * + * A persistence-based 4-digit display is supported. To free up the necessary IO pins + * for driving multiple loads, the pin-saving hardware needs to be in place. + * These extra logic chips (ICs 3 and 4) reduce the number of IO pins + * that are needed to drive the display. The freed-up pins are available at the + * J1-5 connector. The uppermost position has been assigned to drive Load 1, + * the next one down is for Load 2, and the lowest one is for Load 5. The control signal + * for Load 0 is available at the "trigger" connector. + * + * With the green (rev 2.1) version of my PCB, each of the additional outputs has an + * associated ground pin. It is therefore more sensible for those outputs to be active-high + * rather than active-low. With a 5V regulator rather than the normal 3.3V one, these + * outputs are able to drive an SSR directly. energymonitor.org/emon/node/1757 + * + * September 2014: renamed as Mk2_multiLoad_CAT5_3, with these changes: + * - reimplementation of cycleCount, as it could have overflowed with unpredictable results; + * - the functions increaseLoadIfPossible() and decreaseLoadIfPossible() have been tidied; + * - energyThreshold_long has been renamed as midPointOfEnergyBucket_long. + * + * December 2014: renamed as Mk2_multiLoad_CAT5_4, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed); + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances; + * - the logic for each of the 5 additional loads has been inverted by use of the '!' character. + * these outputs are now active-high rather than active-low + * + * November 2015: renamed as Mk2_multiLoad_wired_5, with these changes: + * - the original twin-threshold algorithm for energy state management has been reinstated; + * - improved mechanism for controlling multiple loads (faster and more accurate); + * - the phaseCal mechanism has been reinstated; + * - SWEETZONE_IN_JOULES has been replaced by WORKING_RANGE_IN_JOULES. + * + * January 2015: renamed as Mk2_multiLoad_wired_5a, with this change: + * - minor bug-fix in allGeneralProcessing() which affects how the energy thresholds are adjusted immediately + * after a change of load-state has tqaken place. + * + * January 2015: renamed as Mk2_multiLoad_wired_6, with this change: + * - reinstatement of min & max limits for the energy bucket's level. This section was lost during the + * conversion from version 4 to version 5. The absence of this section prevents diversion from starting + * in the correct manner. Versions 5 and 5a should therefore not be used. Version 6 is believed to be + * a correct implementation of the improved mechanism for controlling multiple loads. + * + * January 2016: renamed as Mk2_multiLoad_wired_6a, with a minor change in the ISR to + * remove a timing uncertainty. + * + * January 2016: updated to Mk2_multiLoad_wired_6b: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * February 2016: updated to Mk2_multiLoad_wired_7, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * + * February 2020: updated to Mk2_multiLoad_wired_7a with these changes: + * - removal of some redundant code in the logic for determining the next load state. + * + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_standardDisplay_3loads_1.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_standardDisplay_3loads_1.txt new file mode 100644 index 0000000..02d6d9d --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_standardDisplay_3loads_1.txt @@ -0,0 +1,40 @@ +/* Mk2_standardDisplay_3loads_1 + * + * This sketch is for diverting suplus PV power using multiple hard-wired loads. It is intended + * for use with my PCB-based hardware for the Mk2 PV Router. + * + * This sketch is only for use when the hardware is in its standard configuration with 14 wire links + * rather than including ICs 3 and 4. The control mode setting is hard-coded prior to compilation, + * options being NORMAL and ANTI_FLICKER. + * + * The integral voltage sensor is fed from one of the secondary coils of the transformer. + * Current is measured via Current Transformers at the CT1 and CT2 ports. + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the 'diverted' current, so that energy which is diverted via any of the dump-loads + * can be recorded and displayed locally. + * + * Up to three loads can be supported, the port allocation section has details of where the control signals + * can be accessed. + * + * (earlier history can be found in my multiLoad_wired sketches) + * + * February 2016: updated to Mk2_multiLoad_wired_7, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * + * February 2020: updated to Mk2_multiLoad_wired_7a with these changes: + * - removal of some redundant code in the logic for determining the next load state. + * + * April 2020: updated to Mk2_standardDisplay_3loads_1 with these changes: + * - change the display configuration to standard (i.e. 14 wire links, not pin-saving hardware) + * - provide 3 loads at ports D4, D3 and D15 (aka A1) + * - control mode is hard-coded to NORMAL, but ANTI_FLICKER mode is still available + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ \ No newline at end of file diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_1.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_1.txt new file mode 100644 index 0000000..d717cf8 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_1.txt @@ -0,0 +1,17 @@ +Detail for my Mk2_withRemoteLoad_1.ino sketch + +This is an upgraded version of the original Mk2 sketch for use with my +PCB-based hardware. It still supports two current sensors, CT1 and CT2. +CT1 is for monitoring the flow of energy at the supply point; CT2 is +available for monitoring the flow of current to the primary dump-load. + +With this version, a secondary dump-load is supported, it being controlled +by the on-board RF facility. By using the pin-saving hardware option, +the 4-digit display is still available for use. + +The primary (wired) load is still controlled from the "trigger" port. In +this sketch, the "mode" port is now used for priority selection. If the +associated switch is open, the local (wired) load has priority. If the +switch is closed, thereby shorting these pins together, the secondary +(RF-controlled)load has priority. + diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_2.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_2.txt new file mode 100644 index 0000000..0916a74 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_2.txt @@ -0,0 +1,31 @@ +Detail for my Mk2_withRemoteLoad_2.ino sketch + +This is an upgraded version of the original Mk2 sketch for use with my +PCB-based hardware. It still supports two current sensors, CT1 and CT2. +CT1 is for monitoring the flow of energy at the supply point; CT2 is +available for monitoring the flow of current to the primary dump-load. + +With this version, a secondary dump-load is supported, it being controlled +by the on-board RF facility. By using the pin-saving hardware option, +the 4-digit display is still available for use. + +The primary (wired) load is still controlled from the "trigger" port. In +this sketch, the "mode" port is now used for priority selection. If the +associated switch is open, the local (wired) load has priority. If the +switch is closed, thereby shorting these pins together, the secondary +(RF-controlled)load has priority. + +The _2 release includes various changes since the _1 version: + +- a REQUIRED_EXPORT_IN_WATTS facility has been added. This acts like a + leak in the energy bucket, therefore reducing the amount of surplus power + that can be diverted. When a negative value is entered, this facility + acts like a PV Simulator, which can be very helful for test purposes. + +- The state of the energy bucket is displayed at the Serial Monitor every + second in Joules. + +- The RFM12B is no longer sent to sleep between RF trnsmissions, it remains + awake throughout. The library call rf12_sendStart() has been replaced by + rf12_sendNow(). These changes are intended to minimise any disruption + to the continuous sampling process when RF transmissions are sent. \ No newline at end of file diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_3.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_3.txt new file mode 100644 index 0000000..62f30e2 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_3.txt @@ -0,0 +1,38 @@ +Detail for my Mk2_withRemoteLoad_3.ino sketch + +This is an upgraded version of the original Mk2 sketch for use with my +PCB-based hardware. It still supports two current sensors, CT1 and CT2. +CT1 is for monitoring the flow of energy at the supply point; CT2 is +available for monitoring the flow of current to the primary dump-load. + +With this version, a secondary dump-load is supported, it being controlled +by the on-board RF facility. By using the pin-saving hardware option, +the 4-digit display is still available for use. + +The primary (wired) load is still controlled from the "trigger" port. In +this sketch, the "mode" port is now used for priority selection. If the +associated switch is open, the local (wired) load has priority. If the +switch is closed, thereby shorting these pins together, the secondary +(RF-controlled)load has priority. + +The _2 release includes various changes since the _1 version: + +- a REQUIRED_EXPORT_IN_WATTS facility has been added. This acts like a + leak in the energy bucket, therefore reducing the amount of surplus power + that can be diverted. When a negative value is entered, this facility + acts like a PV Simulator, which can be very helful for test purposes. + +- The state of the energy bucket is displayed at the Serial Monitor every + second in Joules. + +- The RFM12B is no longer sent to sleep between RF trnsmissions, it remains + awake throughout. The library call rf12_sendStart() has been replaced by + rf12_sendNow(). These changes are intended to minimise any disruption + to the continuous sampling process when RF transmissions are sent. + +Changes for version _3: + +- The 'long' variable, cycleCount, which counted mains cycles since start-up, has + been removed. This variable would have eventually overflowed which could have + caused unpredictable effects. The related functionality has been re-implemented + using individual 'int' counters. \ No newline at end of file diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_4.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_4.txt new file mode 100644 index 0000000..30f24e0 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_4.txt @@ -0,0 +1,49 @@ +Detail for my Mk2_withRemoteLoad_4.ino sketch + +This is an upgraded version of the original Mk2 sketch for use with my +PCB-based hardware. It still supports two current sensors, CT1 and CT2. +CT1 is for monitoring the flow of energy at the supply point; CT2 is +available for monitoring the flow of current to the primary dump-load. + +With this version, a secondary dump-load is supported, it being controlled +by the on-board RF facility. By using the pin-saving hardware option, +the 4-digit display is still available for use. + +The primary (wired) load is still controlled from the "trigger" port. In +this sketch, the "mode" port is now used for priority selection. If the +associated switch is open, the local (wired) load has priority. If the +switch is closed, thereby shorting these pins together, the secondary +(RF-controlled)load has priority. + +The _2 release includes various changes since the _1 version: + +- a REQUIRED_EXPORT_IN_WATTS facility has been added. This acts like a + leak in the energy bucket, therefore reducing the amount of surplus power + that can be diverted. When a negative value is entered, this facility + acts like a PV Simulator, which can be very helful for test purposes. + +- The state of the energy bucket is displayed at the Serial Monitor every + second in Joules. + +- The RFM12B is no longer sent to sleep between RF trnsmissions, it remains + awake throughout. The library call rf12_sendStart() has been replaced by + rf12_sendNow(). These changes are intended to minimise any disruption + to the continuous sampling process when RF transmissions are sent. + +Changes for version _3: + +- The 'long' variable, cycleCount, which counted mains cycles since start-up, has + been removed. This variable would have eventually overflowed which could have + caused unpredictable effects. The related functionality has been re-implemented + using individual 'int' counters. + +Changes for version _4: + +- a persistence check for the zero-crossing detection has been added. This +is to remove any false detections of zero-crossings. This effect is seen more +with some types of transformer than others. + +- a mechanism has been added to monitor and display the minimum number +of sample sets that occur each mains cycle. With a 125us timebase, and three +ADC samples per set, the expected number of sample sets per 20ms mains cycle +is 20 / (3 * 0.125) = 53.33. Any value less than 53 would indicate a loss of data. \ No newline at end of file diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_4a.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_4a.txt new file mode 100644 index 0000000..3b37255 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_4a.txt @@ -0,0 +1,62 @@ +Detail for my Mk2_withRemoteLoad_4a.ino sketch + +This is an upgraded version of the original Mk2 sketch for use with my +PCB-based hardware. It still supports two current sensors, CT1 and CT2. +CT1 is for monitoring the flow of energy at the supply point; CT2 is +available for monitoring the flow of current to the primary dump-load. + +With this version, a secondary dump-load is supported, it being controlled +by the on-board RF facility. By using the pin-saving hardware option, +the 4-digit display is still available for use. + +The primary (wired) load is still controlled from the "trigger" port. In +this sketch, the "mode" port is now used for priority selection. If the +associated switch is open, the local (wired) load has priority. If the +switch is closed, thereby shorting these pins together, the secondary +(RF-controlled)load has priority. + +The _2 release includes various changes since the _1 version: + +- a REQUIRED_EXPORT_IN_WATTS facility has been added. This acts like a + leak in the energy bucket, therefore reducing the amount of surplus power + that can be diverted. When a negative value is entered, this facility + acts like a PV Simulator, which can be very helful for test purposes. + +- The state of the energy bucket is displayed at the Serial Monitor every + second in Joules. + +- The RFM12B is no longer sent to sleep between RF trnsmissions, it remains + awake throughout. The library call rf12_sendStart() has been replaced by + rf12_sendNow(). These changes are intended to minimise any disruption + to the continuous sampling process when RF transmissions are sent. + +Changes for version _3: + +- The 'long' variable, cycleCount, which counted mains cycles since start-up, has + been removed. This variable would have eventually overflowed which could have + caused unpredictable effects. The related functionality has been re-implemented + using individual 'int' counters. + +Changes for version _4: + +- a persistence check for the zero-crossing detection has been added. This +is to remove any false detections of zero-crossings. This effect is seen more +with some types of transformer than others. + +- a mechanism has been added to monitor and display the minimum number +of sample sets that occur each mains cycle. With a 125us timebase, and three +ADC samples per set, the expected number of sample sets per 20ms mains cycle +is 20 / (3 * 0.125) = 53.33. Any value less than 53 would indicate a loss of data. + +Changes for version _4a: + +- A minor change has been made to the function timerIsr() so as to resolve +a timing anomaly that has previously existed. With the new arrangement, a +complete set of data samples is made available by the ISR for use by the +main code. There is no longer any possibility of these values being +overwritten before they are processed. + +- support for the RF69 RF module has been included. + +- the display timeout period has been reduced to 8 hours instead of 10. + diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_4b.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_4b.txt new file mode 100644 index 0000000..b4d00ed --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_4b.txt @@ -0,0 +1,67 @@ +Detail for my Mk2_withRemoteLoad_4b.ino sketch + +This is an upgraded version of the original Mk2 sketch for use with my +PCB-based hardware. It still supports two current sensors, CT1 and CT2. +CT1 is for monitoring the flow of energy at the supply point; CT2 is +available for monitoring the flow of current to the primary dump-load. + +With this version, a secondary dump-load is supported, it being controlled +by the on-board RF facility. By using the pin-saving hardware option, +the 4-digit display is still available for use. + +The primary (wired) load is still controlled from the "trigger" port. In +this sketch, the "mode" port is now used for priority selection. If the +associated switch is open, the local (wired) load has priority. If the +switch is closed, thereby shorting these pins together, the secondary +(RF-controlled)load has priority. + +The _2 release includes various changes since the _1 version: + +- a REQUIRED_EXPORT_IN_WATTS facility has been added. This acts like a + leak in the energy bucket, therefore reducing the amount of surplus power + that can be diverted. When a negative value is entered, this facility + acts like a PV Simulator, which can be very helful for test purposes. + +- The state of the energy bucket is displayed at the Serial Monitor every + second in Joules. + +- The RFM12B is no longer sent to sleep between RF trnsmissions, it remains + awake throughout. The library call rf12_sendStart() has been replaced by + rf12_sendNow(). These changes are intended to minimise any disruption + to the continuous sampling process when RF transmissions are sent. + +Changes for version _3: + +- The 'long' variable, cycleCount, which counted mains cycles since start-up, has + been removed. This variable would have eventually overflowed which could have + caused unpredictable effects. The related functionality has been re-implemented + using individual 'int' counters. + +Changes for version _4: + +- a persistence check for the zero-crossing detection has been added. This +is to remove any false detections of zero-crossings. This effect is seen more +with some types of transformer than others. + +- a mechanism has been added to monitor and display the minimum number +of sample sets that occur each mains cycle. With a 125us timebase, and three +ADC samples per set, the expected number of sample sets per 20ms mains cycle +is 20 / (3 * 0.125) = 53.33. Any value less than 53 would indicate a loss of data. + +Changes for version _4a: + +- A minor change has been made to the function timerIsr() so as to resolve +a timing anomaly that has previously existed. With the new arrangement, a +complete set of data samples is made available by the ISR for use by the +main code. There is no longer any possibility of these values being +overwritten before they are processed. + +- support for the RF69 RF module has been included. + +- the display timeout period has been reduced to 8 hours instead of 10. + + +Changes for version _4b: +- The variables to store ADC data are now declared as "volatile" to remove +any possibility of incorrect operation due to optimisation by the compiler. + diff --git a/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_5.txt b/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_5.txt new file mode 100644 index 0000000..2eecdef --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Mk2_withRemoteLoad_5.txt @@ -0,0 +1,79 @@ +Detail for my Mk2_withRemoteLoad_5.ino sketch + +This is an upgraded version of the original Mk2 sketch for use with my +PCB-based hardware. It still supports two current sensors, CT1 and CT2. +CT1 is for monitoring the flow of energy at the supply point; CT2 is +available for monitoring the flow of current to the primary dump-load. + +With this version, a secondary dump-load is supported, it being controlled +by the on-board RF facility. By using the pin-saving hardware option, +the 4-digit display is still available for use. + +The primary (wired) load is still controlled from the "trigger" port. In +this sketch, the "mode" port is now used for priority selection. If the +associated switch is open, the local (wired) load has priority. If the +switch is closed, thereby shorting these pins together, the secondary +(RF-controlled)load has priority. + +The _2 release includes various changes since the _1 version: + +- a REQUIRED_EXPORT_IN_WATTS facility has been added. This acts like a + leak in the energy bucket, therefore reducing the amount of surplus power + that can be diverted. When a negative value is entered, this facility + acts like a PV Simulator, which can be very helful for test purposes. + +- The state of the energy bucket is displayed at the Serial Monitor every + second in Joules. + +- The RFM12B is no longer sent to sleep between RF trnsmissions, it remains + awake throughout. The library call rf12_sendStart() has been replaced by + rf12_sendNow(). These changes are intended to minimise any disruption + to the continuous sampling process when RF transmissions are sent. + +Changes for version _3: + +- The 'long' variable, cycleCount, which counted mains cycles since start-up, has + been removed. This variable would have eventually overflowed which could have + caused unpredictable effects. The related functionality has been re-implemented + using individual 'int' counters. + +Changes for version _4: + +- a persistence check for the zero-crossing detection has been added. This +is to remove any false detections of zero-crossings. This effect is seen more +with some types of transformer than others. + +- a mechanism has been added to monitor and display the minimum number +of sample sets that occur each mains cycle. With a 125us timebase, and three +ADC samples per set, the expected number of sample sets per 20ms mains cycle +is 20 / (3 * 0.125) = 53.33. Any value less than 53 would indicate a loss of data. + +Changes for version _4a: + +- A minor change has been made to the function timerIsr() so as to resolve +a timing anomaly that has previously existed. With the new arrangement, a +complete set of data samples is made available by the ISR for use by the +main code. There is no longer any possibility of these values being +overwritten before they are processed. + +- support for the RF69 RF module has been included. + +- the display timeout period has been reduced to 8 hours instead of 10. + + +Changes for version _4b: +- The variables to store ADC data are now declared as "volatile" to remove +any possibility of incorrect operation due to optimisation by the compiler. + +Changes for version _5: + - improvements to the start-up logic. The start of normal operation is now + synchronised with the start of a new mains cycle. + - reduce the amount of feedback in the Low Pass Filter for removing the DC content + from the Vsample stream. This resolves an anomaly which has been present since + the start of this project. Although the amount of feedback has previously been + excessive, this anomaly has had minimal effect on the system's overall behaviour. + - removal of the unhelpful "triggerNeedsToBeArmed" mechanism + - tidying of the "confirmPolarity" logic to make its behaviour more clear + - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + + diff --git a/docs/routers/mk2pvrouter.co.uk/About_RST_375us_dev.txt b/docs/routers/mk2pvrouter.co.uk/About_RST_375us_dev.txt new file mode 100644 index 0000000..79924a5 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_RST_375us_dev.txt @@ -0,0 +1,32 @@ +Detail for my sketch, RST_375us_dev.ino + +/* February 2014 + * Tool to capture the raw V and I samples generated by the Atmega 328P processor + * during one or more mains cycles. The data is displayed on the Serial Monitor. + * + * Voltage samples are displayed as 'v' + * Current samples via CT1 are displayed as '1' + * + * The display is more compact if not every set of samples is shown. This aspect + * can be changed at the second of the two lines of code which contain a '%' character. + * + * February 2021 + * In the original version, data samples were obtained using the analogRead() function. Now, + * they are obtained by the ADC being controlled by a hardware timer with a periodicity of 125 us, + * hence a full set of 1 x V and 2 x I samples takes 375 us. The same scheme for collecting + * data samples is found in many of my Mk2 PV Router sketches. + * + * When used with an output stage that has zero-crossing detection, the signal at port D4 can + * be used to activate a load for just a single half main cycle. The behaviour of the output signal + * from CT1 can then be studied in detail. + * + * The stream of raw data samples from any floating CT will always be distorted because the CT acts as + * a High Pass Filter. This effect is only noticeable when the current that is being measured changes, + * such as when an electrical load is turned on or off. This sketch includes additional software which + * compensates for this effect. Similar compensation software has been introduced to the varous + * "fasterControl" sketches that now exist. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + * June 2021 + */ \ No newline at end of file diff --git a/docs/routers/mk2pvrouter.co.uk/About_RawSamplesTool_2chan.txt b/docs/routers/mk2pvrouter.co.uk/About_RawSamplesTool_2chan.txt new file mode 100644 index 0000000..392815b --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_RawSamplesTool_2chan.txt @@ -0,0 +1,25 @@ +/* + * Tool to capture the raw samples generated by the Atmega 328P processor + * during one or more mains cycles. The data is displayed on the Serial Monitor, + * and is also available for subsequent processing using a spreadsheet. + * + * This version is based on similar code that I posted in December 2012 on the + * OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * The pin-allocations have been changed to suit my PCB-based hardware for the + * Mk2 PV Router. The integral voltage sensor is fed from one of the secondary + * coils of the transformer. Current can be measured via Current Transformers + * at the CT1 and CT1 ports. + * + * Voltage samples are displayed as 'v' + * Current samples via CT1 are displayed as '1' + * Current samples via CT2 are displayed as '2' + * + * The display is more compact if not every set of samples is shown. This aspect + * can be changed at the second of the two lines which contain a '%' character. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + * February 2014 + */ \ No newline at end of file diff --git a/docs/routers/mk2pvrouter.co.uk/About_RawSamplesTool_6chan.txt b/docs/routers/mk2pvrouter.co.uk/About_RawSamplesTool_6chan.txt new file mode 100644 index 0000000..1d7c379 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_RawSamplesTool_6chan.txt @@ -0,0 +1,22 @@ +/* + * Tool to capture the raw samples generated by the Atmega 328P processor + * during one or more mains cycles. The data is displayed on the Serial Monitor. + * + * The pin-allocations have been arranged to suit my PCB-based hardware for the + * 3-phase Mk2 PV Router. The six analog ports of the Atmega 328 processor are assigned + * to the three pairs of AC voltage and current measuring channels. + * + * The voltage and current waveforms for phases L1, L2 and L3 respectively are denoted + * '0' - '5' on the output display. + * + * The display is more compact if not every set of samples is shown. This aspect + * can be changed at the second of the two lines which contain a '%' character. + * + * Pauses after each set of measurements has been taken. Press 'g', then [cr], + * to repeat. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + * March 2021 + */ + \ No newline at end of file diff --git a/docs/routers/mk2pvrouter.co.uk/About_Transformer_Checker.txt b/docs/routers/mk2pvrouter.co.uk/About_Transformer_Checker.txt new file mode 100644 index 0000000..b4d3848 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_Transformer_Checker.txt @@ -0,0 +1,26 @@ +/* Transformer_Checker is based on Mk2_RF_datalog_3.ino + * + * Every 1-phase Mk2 PV Router control board has a mains transformer with two secondary outputs. One output provides + * a low-voltage replica of the AC mains voltage; the other is rectified to provide a low-voltage DC supply for the + * processor. Although the power consumption of the Atmel 328P processor is fairly constant, it will be increase + * whenever the output stage is activated. The increased draw from the DC supply will cause the amplitude of the AC signal + * from the other output to slightly decrease. + * + * This sketch can be used to quantify the above effect. A standard output stage should be connected to the primary + * output port but no AC load should be connected otherwise a consequent reduction in the local mains voltage + * could adversely affect this test. + * + * Via the Serial Monitor, this sketch will display the percentage reduction in the measured Vrms value whenever + * the output stage is activated. By adding an extra LED which operates in anti-phase with the primary output, the + * reduction in Vrms can be effectively eliminated. Both LEDs can be driven by the same output port but with their other + * terminals connected to opposite power rails via series resistors of appropriate values. + * + * Any reduction in the measured Vrms value when the output stage is activated represents a non-linearity which will + * result in less than ideal performance. By means of this sketch, an extra LED and series resistor can be used to + * minimise any such effect. + * + * July 2021: first release. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ \ No newline at end of file diff --git a/docs/routers/mk2pvrouter.co.uk/About_cal_CT1_v_meter.txt b/docs/routers/mk2pvrouter.co.uk/About_cal_CT1_v_meter.txt new file mode 100644 index 0000000..c5b94a8 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_cal_CT1_v_meter.txt @@ -0,0 +1,17 @@ +Detail for my cal_CT1_v_meter.ino sketch + +/* cal_CT1_v_meter.ino + * + * February 2018 + * This calibration sketch is based on Mk2_bothDisplays_4.ino. Its purpose is to + * mimic the behaviour of a digital electricity meter. + * + * CT1 should be clipped around one of the live cables that pass through the + * meter. The energy flow measured by CT1 is noted and a short pulse is generated + * whenever a pre-set amount of energy has been recorded (normally 3600J). + * + * This stream of pulses can then be compared against optical pulses from a standard + * electrical utility meter. The pulse rate can be varied by adjusting the value + * of powerCal_grid. When the two streams of pulses are in synch, correct calibration + * of the CT1 channel has been achieved. + diff --git a/docs/routers/mk2pvrouter.co.uk/About_cal_CT2_v_CT1.txt b/docs/routers/mk2pvrouter.co.uk/About_cal_CT2_v_CT1.txt new file mode 100644 index 0000000..37ed326 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_cal_CT2_v_CT1.txt @@ -0,0 +1,23 @@ +Detail for my cal_CT2_v_CT1.ino sketch + +/* cal_CT2_v_CT1.ino + * + * February 2018 + * This calibration sketch is based on Mk2_bothDisplays_4.ino. Its purpose is to + * mimic the behaviour of a dual channel electricity meter. The sensitivity of the + * CT2 channel can then be adjusted to match that of the CT1 channel. Before using + * this sketch, the CT1 channel would normally have been calibrated against the + * user's electricity meter. + * + * CT1 and CT2 should be fitted around the same current-carrying conductor. If + * CT2 has been built into a completed system, the bypass switch can be used to force + * power down that path. + * + * The energy flow on each channel is noted and a short pulse is generated whenever a + * pre-set amount of energy has been recorded (normally 3600J). The two streams of + * pulses can then be compared. The pulse rate for the CT2 channel can be varied by + * adjusting the value of powerCal_diverted. When the two streams of pulses are in + * synch, correct calibration of the CT2 channel has been achieved. + * + * The two pulse streams can be synchronised at any time by earthing R11 which is + * tracked to port A0 (aka D14). diff --git a/docs/routers/mk2pvrouter.co.uk/About_cal_bothDisplays_3.txt b/docs/routers/mk2pvrouter.co.uk/About_cal_bothDisplays_3.txt new file mode 100644 index 0000000..25dfb06 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_cal_bothDisplays_3.txt @@ -0,0 +1,48 @@ +Detail for my cal_bothDisplays_3.ino sketch + +/* cal_bothDisplays_3.ino + * + * This sketch provides an easy way of calibrating the current sensors that are + * facilitated via the CT1 and CT2 ports. Channel selection is provided by a + * switch at the "mode" connector (digital IO port D3) on the control board. + * The default selection is CT1; CT2 is selected when the switch is closed. + * The measured value is shown on the 4-digit display, and also at the Serial Monitor. + * + * CT1 normally monitors the power at the grid supply point. + * CT2 normaly monitors power that is sent to the dump load(s). + * + * For this test, the selected CT should be clipped around a lead through which a known + * amount of power is flowing. This can be compared against the displayed value + * which is proportional to the powerCal setting. Once the optimal powerCal values + * have been obtained for each channel, these values can be transferred into the + * main Mk2 PV Router sketch. + * + * Depending on which way around the CT is connected, the measured value may be + * positive or negative. If it is negative, the display will either flash or + * display a negative symbol. Its behaviour will depend on the way that the display + * has been configured. + * + * The 4-digit display can be driven in two different ways, one with an extra pair + * of logic chips, and one without. The appropriate version of the sketch must be + * selected by including or commenting out the "#define PIN_SAVING_HARDWARE" + * statement near the top of the code. + * + * With the pin-saving logic, the display is not able to show a '-' symbol. But + * in the alternative mode, it is. + * + * December 2017, upgraded to cal_bothDisplays_2: + * In the original version, the mains cycle counter cycled through the values 0 to + * CYCLES_PER_SECOND inclusive so data was only processed every (CYCLES_PER_SECOND + 1) + * mains cycles. Because the accumulated energy data was divided by CYCLES_PER_SECOND, + * there was an error of approx 2% in the displayed power value. In version 2, + * the logic has been corrected to avoid this error. + * + * June 2022, upgraded to cal_bothDisplays_3: + * The 4-digit display still shows the power that is measured on the selected CT channel. The + * Serial monitor however now shows the average power at both CT channels. Previously, the value + * for the selected channel was shown twice. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + * June 2022 + */ diff --git a/docs/routers/mk2pvrouter.co.uk/About_demo_bothDisplays.txt b/docs/routers/mk2pvrouter.co.uk/About_demo_bothDisplays.txt new file mode 100644 index 0000000..b5d4fca --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_demo_bothDisplays.txt @@ -0,0 +1,9 @@ +Detail for my demo_bothDisplays.ino sketch + +This sketch comprises just the display section of my Mk2 PV Router code. +Its intended purpose was to provide an easy way of checking the operation +of the 4-digit display, but the later sketch segCheck_bothDisplays.ino +is more suitable for this task. + +This original version is mentioned in the Build Guide so can remain here +for completeness. \ No newline at end of file diff --git a/docs/routers/mk2pvrouter.co.uk/About_mainBoard_Circuit_1.txt b/docs/routers/mk2pvrouter.co.uk/About_mainBoard_Circuit_1.txt new file mode 100644 index 0000000..745a77d --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_mainBoard_Circuit_1.txt @@ -0,0 +1,4 @@ +Notes re. the original circuit diagram for my (orange) 'main' PCB @ rev 1.1 + +The original circuit diagram has R1 = 10K which is outside the +specified range for the Atmega 328P processor. diff --git a/docs/routers/mk2pvrouter.co.uk/About_main_rev1_circuit_2.txt b/docs/routers/mk2pvrouter.co.uk/About_main_rev1_circuit_2.txt new file mode 100644 index 0000000..b66cfd2 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_main_rev1_circuit_2.txt @@ -0,0 +1,7 @@ +Notes re. my main_rev1_circuit_2 diagram + +This diagram is for the original (orange) PCB @ rev 1.1 + +The diagram was updated in October 2015 to show the value of R1 +being increased from 10K to 47K. This change is in-line with +the published specification for the Atmega 328P processor. diff --git a/docs/routers/mk2pvrouter.co.uk/About_main_rev2_cctDiagram.txt b/docs/routers/mk2pvrouter.co.uk/About_main_rev2_cctDiagram.txt new file mode 100644 index 0000000..ae79e5a --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_main_rev2_cctDiagram.txt @@ -0,0 +1,7 @@ +Notes re. my main_rev2_circuit_2 diagram + +This diagram is for the later (green) PCB @ rev 2.1 + +The diagram was updated in October 2015 to show the value of R1 +being increased from 10K to 47K. This change is in-line with +the published specification for the Atmega 328P processor. diff --git a/docs/routers/mk2pvrouter.co.uk/About_main_rev4.1_cctDiagram.txt b/docs/routers/mk2pvrouter.co.uk/About_main_rev4.1_cctDiagram.txt new file mode 100644 index 0000000..97e8261 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_main_rev4.1_cctDiagram.txt @@ -0,0 +1,5 @@ +Notes re. my main_rev4.1_circuit diagram + +This diagram is for the latest (black) 1-phase control board @ rev 4.1 + +There are no known problems with this board or its circuit digram diff --git a/docs/routers/mk2pvrouter.co.uk/About_remoteUnit_fasterControl_1.txt b/docs/routers/mk2pvrouter.co.uk/About_remoteUnit_fasterControl_1.txt new file mode 100644 index 0000000..0c2f721 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_remoteUnit_fasterControl_1.txt @@ -0,0 +1,52 @@ +Detail for my sketch remoteUnit_fasterControl_1.ino + +/* remoteUnit_fasterControl_1.ino + * + * This sketch is to control a remote load for a Mk2 PV Router at the receiver end + * of an RF link. If RF transmission is lost, the load is turned off. A repeater + * signal is available at the 'mode' connector. This is intended to drive an LED + * with an appropriate series resistor, e.g. 120R. + * + * The ability to measure and display the amount of energy which has been diverted + * via the remote load is included. For this to happen, one of the live cores + * needs to pass through a CT which connects to the 'CT2' connector. + * + * The 'CT1' connector has been re-used in this sketch to provide a 2-colour + * indication of the state of the RF link. A schematic for this circuit may be + * found immediately below this header. + * + * A persistence-based 4-digit display is supported. When the RFM12B module is + * in use, the display can only be used in conjunction with an extra pair of + * logic chips. These are ICs 3 and 4, which reduce the number of processor pins + * that are needed to drive the display. + * + * This sketch is similar in function to RF_for_Mk2_rx.ino, as posted on the + * OpenEnergyMonitor forum. That version, and other related material, can be + * found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * January 2016: renamed as remote_Mk2_receiver_1a, with a minor change in the ISR to + * remove a timing uncertainty. Support for the RF69 RF module has also been included. + * + * January 2016: updated to remote_Mk2_receiver_1b: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * February 2016: updated to remote_Mk2_receiver_2, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - change all instances of "triac" to "load" + * + * September 2022: updated to remoteUnit_fasterConrol_1, with this change: + * - RF payload reduced to just one integer for the load state. For use with the transmitter + * sketch, Mk2_fasterControl_withRemoteLoad_n + * - the hardware timer that controls the ADC has been increased from 200 to 250 us (just to + * reduce the workload). + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + diff --git a/docs/routers/mk2pvrouter.co.uk/About_remote_Mk2_receiver_1.txt b/docs/routers/mk2pvrouter.co.uk/About_remote_Mk2_receiver_1.txt new file mode 100644 index 0000000..cd6bbd2 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_remote_Mk2_receiver_1.txt @@ -0,0 +1,21 @@ +Detail for my remote_Mk2_receiver_1.ino sketch + +This sketch is for use as the receiver part of a Mk2 PV Router system +that is operated via an RF link rather than via a control cable. It is +intended for use with my PCB-based hardware. + +One current sensor is supported at the CT2 port. This can be used for +monitoring the power that is diverted by the receiver unit. The +diverted energy total in kWh is available at the 4-digit display connector. +The display returns to its idle state after a period of 10 hours during +which the control signal has never been in the 'on' state. + +The CT1 port has been re-allocated for driving a pair of LEDs to show +the status of the RF control link. The circuit for this feature is shown +in the header of the sketch. + +The output signal for controlling the load, as instructed by the +associated transmitter, is available at the "mode" port (D3) in active-high +format and at the "trigger" port (D4) in active-low format. + +The corresponding transmitter sketch is: Mk2_withRemoteLoad_n.ino diff --git a/docs/routers/mk2pvrouter.co.uk/About_remote_Mk2_receiver_1a.txt b/docs/routers/mk2pvrouter.co.uk/About_remote_Mk2_receiver_1a.txt new file mode 100644 index 0000000..8d67f7b --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_remote_Mk2_receiver_1a.txt @@ -0,0 +1,30 @@ +Detail for my remote_Mk2_receiver_1a.ino sketch + +This sketch is for use as the receiver part of a Mk2 PV Router system +that is operated via an RF link rather than via a control cable. It is +intended for use with my PCB-based hardware. + +One current sensor is supported at the CT2 port. This can be used for +monitoring the power that is diverted by the receiver unit. The +diverted energy total in kWh is available at the 4-digit display connector. +The display returns to its idle state after a period of 10 hours during +which the control signal has never been in the 'on' state. + +The CT1 port has been re-allocated for driving a pair of LEDs to show +the status of the RF control link. The circuit for this feature is shown +in the header of the sketch. + +The output signal for controlling the load, as instructed by the +associated transmitter, is available at the "mode" port (D3) in active-high +format and at the "trigger" port (D4) in active-low format. + +The corresponding transmitter sketch is: Mk2_withRemoteLoad_n.ino + +Changes for version _1a: +- A minor change has been made to the function timerIsr() so as to resolve +a timing anomaly that has previously existed. With the new arrangement, a +complete set of data samples is made available by the ISR for use by the +main code. There is no longer any possibility of these values being +overwritten before they are processed. + +- the display timeout period has been reduced to 8 hours instead of 10. diff --git a/docs/routers/mk2pvrouter.co.uk/About_remote_Mk2_receiver_1b.txt b/docs/routers/mk2pvrouter.co.uk/About_remote_Mk2_receiver_1b.txt new file mode 100644 index 0000000..72dc79c --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_remote_Mk2_receiver_1b.txt @@ -0,0 +1,35 @@ +Detail for my remote_Mk2_receiver_1b.ino sketch + +This sketch is for use as the receiver part of a Mk2 PV Router system +that is operated via an RF link rather than via a control cable. It is +intended for use with my PCB-based hardware. + +One current sensor is supported at the CT2 port. This can be used for +monitoring the power that is diverted by the receiver unit. The +diverted energy total in kWh is available at the 4-digit display connector. +The display returns to its idle state after a period of 10 hours during +which the control signal has never been in the 'on' state. + +The CT1 port has been re-allocated for driving a pair of LEDs to show +the status of the RF control link. The circuit for this feature is shown +in the header of the sketch. + +The output signal for controlling the load, as instructed by the +associated transmitter, is available at the "mode" port (D3) in active-high +format and at the "trigger" port (D4) in active-low format. + +The corresponding transmitter sketch is: Mk2_withRemoteLoad_n.ino + +Changes for version _1a: +- A minor change has been made to the function timerIsr() so as to resolve +a timing anomaly that has previously existed. With the new arrangement, a +complete set of data samples is made available by the ISR for use by the +main code. There is no longer any possibility of these values being +overwritten before they are processed. + +- the display timeout period has been reduced to 8 hours instead of 10. + +Changes for version _1b: +- The variables to store ADC data are now declared as "volatile" to remove +any possibility of incorrect operation due to optimisation by the compiler. + diff --git a/docs/routers/mk2pvrouter.co.uk/About_remote_Mk2_receiver_2.txt b/docs/routers/mk2pvrouter.co.uk/About_remote_Mk2_receiver_2.txt new file mode 100644 index 0000000..2f7b902 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/About_remote_Mk2_receiver_2.txt @@ -0,0 +1,47 @@ +Detail for my remote_Mk2_receiver_2.ino sketch + +This sketch is for use as the receiver part of a Mk2 PV Router system +that is operated via an RF link rather than via a control cable. It is +intended for use with my PCB-based hardware. + +One current sensor is supported at the CT2 port. This can be used for +monitoring the power that is diverted by the receiver unit. The +diverted energy total in kWh is available at the 4-digit display connector. +The display returns to its idle state after a period of 10 hours during +which the control signal has never been in the 'on' state. + +The CT1 port has been re-allocated for driving a pair of LEDs to show +the status of the RF control link. The circuit for this feature is shown +in the header of the sketch. + +The output signal for controlling the load, as instructed by the +associated transmitter, is available at the "mode" port (D3) in active-high +format and at the "trigger" port (D4) in active-low format. + +The corresponding transmitter sketch is: Mk2_withRemoteLoad_n.ino + +Changes for version _1a: +- A minor change has been made to the function timerIsr() so as to resolve +a timing anomaly that has previously existed. With the new arrangement, a +complete set of data samples is made available by the ISR for use by the +main code. There is no longer any possibility of these values being +overwritten before they are processed. + +- the display timeout period has been reduced to 8 hours instead of 10. + +Changes for version _1b: +- The variables to store ADC data are now declared as "volatile" to remove +any possibility of incorrect operation due to optimisation by the compiler. + +Changes for version _2: + January 2016: updated to remote_Mk2_receiver_2, with these changes: + - improvements to the start-up logic. The start of normal operation is now + synchronised with the start of a new mains cycle. + - reduce the amount of feedback in the Low Pass Filter for removing the DC content + from the Vsample stream. This resolves an anomaly which has been present since + the start of this project. Although the amount of feedback has previously been + excessive, this anomaly has had minimal effect on the system's overall behaviour. + - change all instances of "triac" to "load" + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_1.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_1.ino new file mode 100644 index 0000000..41b134c --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_1.ino @@ -0,0 +1,852 @@ +/* Mk2_3phase_RFdatalog_1 + * + * This sketch provides continuous monitoring of real power on three phases. + * Surplus power is diverted to multiple loads in sequential order. + * Datalogging of real power and Vrms is provided for each phase. + * The presence or absence of the RFM12B needs to be set at compile time + * + * Robin Emley + * www.Mk2PVrouter.co.uk + * January 2015 + */ + +#include // may not be needed, but it's probably a good idea to include this + +//#define RF_PRESENT // <- this line should be commented out if the RFM12B module is not present + +#ifdef RF_PRESENT +#include +#endif + +// In this sketch, the ADC is free-running with a cycle time of ~104uS. + +// WORKLOAD_CHECK is available for determining how much spare processing time there +// is. To activate this mode, the #define line below should be included: +//#define WORKLOAD_CHECK + +#define CYCLES_PER_SECOND 50 +#define JOULES_PER_WATT_HOUR 3600 // may be needed for datalogging +#define SWEETZONE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator +#define NO_OF_PHASES 3 +#define TRIAC_ON LOW +#define TRIAC_OFF HIGH +#define DATALOG_PERIOD_IN_SECONDS 2 + +const byte noOfDumploads = 3; // The logic expects a minimum of 2 dumploads, + // for local & remote loads, but neither has to + // be physically present. + +enum polarities {NEGATIVE, POSITIVE}; +enum outputModes {ANTI_FLICKER, NORMAL}; +enum loadPriorityModes {SWITCHED_PRIORITIES, NORMAL_PRIORITIES}; + +enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are active low +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +enum energyStates {LOWER_HALF, UPPER_HALF}; // for single threshold AF algorithm +enum energyStates energyStateNow; + +// For this multi-load version, the same mechanism has been retained but the +// output mode is hard-coded as below: +enum outputModes outputMode = ANTI_FLICKER; + +// In this multi-load version, the external switch is re-used to determine the load priority +enum loadPriorityModes loadPriorityMode; + +#ifdef RF_PRESENT +#define freq RF12_433MHZ // Use the freq to match the module you have. + +const int nodeID = 10; +const int networkGroup = 210; +const int UNO = 1; +#endif + +typedef struct { + int power_L1; + int power_L2; + int power_L3; + int Vrms_L1; + int Vrms_L2; + int Vrms_L3;} Tx_struct; // revised data for RF comms +Tx_struct tx_data; + + +// ----------- Pinout assignments ----------- +// +// digital pins: +const byte loadPrioritySelectorPin = 3; // // for 3-phase PCB +const byte physicalLoad_0_pin = 8; // for 3-phase PCB, Load #1 +const byte physicalLoad_1_pin = 7; // for 3-phase PCB, Load #2 +const byte physicalLoad_2_pin = 6; // for 3-phase PCB, Load #3 + +// analogue input pins +const byte sensorV[NO_OF_PHASES] = {0,2,4}; // for 3-phase PCB +const byte sensorI[NO_OF_PHASES] = {1,3,5}; // for 3-phase PCB + + +// -------------- general global variables ----------------- +// +// Some of these variables are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPeriod = false; // start-up delay, allows things to settle +byte initialDelay = 3; // in seconds, to allow time to open the Serial monitor +byte startUpPeriod = 3; // in seconds, to allow LP filter to settle + +long DCoffset_V_long[NO_OF_PHASES]; // <--- for LPF +long DCoffset_V_min; // <--- for LPF (min limit) +long DCoffset_V_max; // <--- for LPF (max limit) +int DCoffset_I_nom; // nominal mid-point value of ADC @ x1 scale + +int postMidPointCrossingDelay_cycles; // assigned in setup(), different for each output mode +const int postMidPointCrossingDelayForAF_cycles = 10; // in 20 ms counts +const int interLoadSeparationDelay_cycles = 25; // in 20 ms cycle counts +byte activeLoadID; // only one load may operate freely at a time. +int mainsCyclesSinceLastMidPointCrossing = 0; +int mainsCyclesSinceLastChangeOfLoadState = 0; +float energyStateAtLastTransition; + +// for interaction between the main processor and the ISR +volatile boolean dataReady = false; +int sampleV[NO_OF_PHASES]; +int sampleI[NO_OF_PHASES]; + + +// Calibration values +//------------------- +// Three calibration values are used in this sketch: powerCal, phaseCal and voltageCal. +// With most hardware, the default values are likely to work fine without +// need for change. A compact explanation of each of these values now follows: + +// When calculating real power, which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal[NO_OF_PHASES] = {0.0435, 0.043, 0.043}; +//const float powerCal[NO_OF_PHASES] = {0.05}; + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// NB. Any tool which determines the optimal value of phaseCal must have a similar +// scheme for taking sample values as does this sketch! +// +const float phaseCal[NO_OF_PHASES] = {0.5, 0.5, 0.5}; // <- nominal values only + +// For datalogging purposes, voltageCal has been added too. Because the range of ADC values is +// similar to the actual range of volts, the optimal value for this cal factor is likely to be +// close to unity. +const float voltageCal[NO_OF_PHASES] = {1.03, 1.03, 1.03}; // compared with Fluke 77 meter + + +int phaseCal_int[NO_OF_PHASES]; // to avoid the need for floating-point maths + +float energyBucketCapacity_main; +float energyBucketCapacity_perPhase; +float energyInBucket_main; +float midPointOfMainEnergyBucket; +boolean energyLevelInUpperHalf; + +// per-phase items +float energyStateOfPhase[NO_OF_PHASES]; // an energy bucket per phase +float energyBucketLevel_perPhase_max; // for restraining the per-phase energy bucket levels +float energyBucketLevel_perPhase_min; // for restraining the per-phase energy bucket levels + +int datalogCountInMainsCycles; +const int maxDatalogCountInMainsCycles = DATALOG_PERIOD_IN_SECONDS * CYCLES_PER_SECOND; + + +void setup() +{ + delay (initialDelay * 1000); // allows time to open the Serial Monitor + + Serial.begin(9600); // initialize Serial interface + Serial.println(); + Serial.println(); + Serial.println(); + Serial.println("----------------------------------"); + Serial.println("Sketch ID: Mk2_3phase_RFdatalog_1.ino"); + + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for Load #1 + pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for Load #2 + pinMode(physicalLoad_2_pin, OUTPUT); // driver pin for Load #3 + + for(int i = 0; i< noOfDumploads; i++) + { + logicalLoadState[i] = LOAD_OFF; + physicalLoadState[i] = LOAD_OFF; + } + + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // update the local load's state. + digitalWrite(physicalLoad_1_pin, physicalLoadState[1]); // update the additional load state (inverse logic). + digitalWrite(physicalLoad_2_pin, physicalLoadState[2]); // update the additional load state (inverse logic). + + pinMode(loadPrioritySelectorPin, INPUT); + digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and + loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority mode + + for (byte phase = 0; phase < NO_OF_PHASES; phase++) + { + // When using integer maths, calibration values that have been supplied in + // floating point form need to be rescaled. + phaseCal_int[phase] = phaseCal[phase] * 256; // for integer maths + DCoffset_V_long[phase] = 512L * 256; // nominal mid-point value of ADC @ x256 scale + } + + // Define operating limits for the LP filters which identify DC offset in the voltage + // sample streams. By limiting the output range, these filters always should start up + // correctly. + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + DCoffset_I_nom = 512; // nominal mid-point value of ADC @ x1 scale + + // for the main energy bucket + energyBucketCapacity_main = (float)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND; + midPointOfMainEnergyBucket = energyBucketCapacity_main * 0.5; // for resetting flexible thresholds + energyInBucket_main = 0; + energyStateAtLastTransition = midPointOfMainEnergyBucket; + + // for the per-phase energy buckets + energyBucketCapacity_perPhase = (float)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND; + energyBucketLevel_perPhase_max = energyBucketCapacity_perPhase * 0.5; + energyBucketLevel_perPhase_min = energyBucketCapacity_perPhase * -0.5; + + Serial.println ("ADC mode: free-running"); + Serial.print ("requiredExport in Watts = "); + Serial.println (REQUIRED_EXPORT_IN_WATTS); + + // Set up the ADC to be free-running + ADCSRA = (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples (3 pairs). The main processor and +// the ADC work autonomously, their operation being only linked via the dataReady flag. +// As soon as data is made available by the ADC, the main processor can start to work +// on it immediately. +// +void processRawSamples() +{ + static long sumP[NO_OF_PHASES]; + static enum polarities polarityOfLastSampleV[NO_OF_PHASES]; // for zero-crossing detection + static long lastSampleV_minusDC_long[NO_OF_PHASES]; // for the phaseCal algorithm + static long cumVdeltasThisCycle_long[NO_OF_PHASES]; // for the LPF which determines DC offset (voltage) + static int samplesDuringThismainsCycle[NO_OF_PHASES]; + static long sum_Vsquared[NO_OF_PHASES]; + static long samplesDuringThisDatalogPeriod; + enum polarities polarityNow; + float realPower; + + // The raw V and I samples are processed in "phase pairs" + for (byte phase = 0; phase < NO_OF_PHASES; phase++) + { + // remove DC offset from each raw voltage sample by subtracting the accurate value + // as determined by its associated LP filter. + long sampleV_minusDC_long = ((long)sampleV[phase]<<8) - DCoffset_V_long[phase]; + + // determine polarity, to aid the logical flow + if(sampleV_minusDC_long > 0) { + polarityNow = POSITIVE; } + else { + polarityNow = NEGATIVE; } + + if (polarityNow == POSITIVE) + { + if (beyondStartUpPeriod) + { + if (polarityOfLastSampleV[phase] != POSITIVE) + { + /* This is the start of a new +ve half cycle, for this phase, just after the + * zero-crossing point. Before the contribution from this phase can be added + * to the running total, the cal factor for this phase must be applied. + */ + realPower = (float)(sumP[phase] / samplesDuringThismainsCycle[phase]) * powerCal[phase]; + + processLatestContribution(phase, realPower); // runs at 6.6 ms intervals + + sumP[phase] = 0; + samplesDuringThismainsCycle[phase] = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + if ((phase == 0) && samplesDuringThismainsCycle[0] == 5) + { + // This code is executed once per 20mS, shortly after the start of each new + // mains cycle on phase 0. + // +// cycleCount++; + mainsCyclesSinceLastMidPointCrossing++; + mainsCyclesSinceLastChangeOfLoadState++; + datalogCountInMainsCycles++; + + /* Now it's time to determine whether any of the the loads need to be changed. + * This is a 2-stage process: + * First, change the LOGICAL loads as necessary, then update the PHYSICAL + * loads according to the mapping that exists between them. The mapping is + * 1:1 by default but can be altered by a hardware switch which allows the + * priorities of the first two load to be swapped. + * This code uses a single-threshold algorithm which relies on regular switching + * of the load to maintain the energy balance within the permitted range. + */ + + // when using the single-threshold power diversion algorithm, a counter needs + // to be reset whenever the energy level in the accumulator crosses the mid-point + // + enum energyStates energyStateOnLastLoop = energyStateNow; + + if (energyInBucket_main > midPointOfMainEnergyBucket) { + energyStateNow = UPPER_HALF; } + else { + energyStateNow = LOWER_HALF; } + + if (energyStateNow != energyStateOnLastLoop) { + mainsCyclesSinceLastMidPointCrossing = 0; } + + + if (mainsCyclesSinceLastMidPointCrossing > postMidPointCrossingDelay_cycles) + { + if (energyStateNow == UPPER_HALF) + { + increaseLoadIfPossible(); // to reduce the level in the bucket + } + else + { + decreaseLoadIfPossible(); // to increase the level in the bucket + } + } + + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + // update all the physical loads + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger + digitalWrite(physicalLoad_1_pin, physicalLoadState[1]); // active low for trigger + digitalWrite(physicalLoad_2_pin, physicalLoadState[2]); // active low for trigger + + if (datalogCountInMainsCycles >= maxDatalogCountInMainsCycles) + { + datalogCountInMainsCycles = 0; + + tx_data.power_L1 = energyStateOfPhase[0] / maxDatalogCountInMainsCycles; + tx_data.power_L2 = energyStateOfPhase[1] / maxDatalogCountInMainsCycles; + tx_data.power_L3 = energyStateOfPhase[2] / maxDatalogCountInMainsCycles; + tx_data.Vrms_L1 = (int)(voltageCal[0] * sqrt(sum_Vsquared[0] / samplesDuringThisDatalogPeriod)); + tx_data.Vrms_L2 = (int)(voltageCal[1] * sqrt(sum_Vsquared[1] / samplesDuringThisDatalogPeriod)); + tx_data.Vrms_L3 = (int)(voltageCal[2] * sqrt(sum_Vsquared[2] / samplesDuringThisDatalogPeriod)); +#ifdef RF_PRESENT + send_rf_data(); +#endif +// + Serial.print(energyInBucket_main / CYCLES_PER_SECOND); + Serial.print(", "); + Serial.print(tx_data.power_L1); + Serial.print(", "); + Serial.print(tx_data.power_L2); + Serial.print(", "); + Serial.print(tx_data.power_L3); + Serial.print(", "); + Serial.print(tx_data.Vrms_L1); + Serial.print(", "); + Serial.print(tx_data.Vrms_L2); + Serial.print(", "); + Serial.println(tx_data.Vrms_L3); + // + energyStateOfPhase[0] = 0; + energyStateOfPhase[1] = 0; + energyStateOfPhase[2] = 0; + sum_Vsquared[0] = 0; + sum_Vsquared[1] = 0; + sum_Vsquared[2] = 0; + samplesDuringThisDatalogPeriod = 0; + } + } + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (initialDelay + startUpPeriod) * 1000) + { + beyondStartUpPeriod = true; + Serial.println ("Go!"); + } + } + } // end of processing that is specific to samples where the voltage is positive + + else // the polarity of this sample is negative + { + if (polarityOfLastSampleV[phase] != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // This is a convenient point to update the Low Pass Filter for the phase that is + // being processed. This needs to be done right from the start. + // + DCoffset_V_long[phase] += (cumVdeltasThisCycle_long[phase]>>6); // faster than * 0.01 + cumVdeltasThisCycle_long[phase] = 0; + + // To ensure that this LP filter will always start up correctly when 240V AC is + // available, its output value needs to be prevented from drifting beyond the likely range + // of the voltage signal. + // + if (DCoffset_V_long[phase] < DCoffset_V_min) { + DCoffset_V_long[phase] = DCoffset_V_min; } + else + if (DCoffset_V_long[phase] > DCoffset_V_max) { + DCoffset_V_long[phase] = DCoffset_V_max; } + + checkLoadPrioritySelection(); // updates load priorities if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is negitive + + // Processing for EVERY pair of samples. Most of this code is not used during the + // start-up period, but it does no harm to leave it in place. Accumulated values + // are cleared when beyondStartUpPhase is set to true. + // + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_long = ((long)(sampleI[phase] - DCoffset_I_nom))<<8; + + // phase-shift the voltage waveform so that it aligns with the current when a + // resistive load is used + long phaseShiftedSampleV_minusDC_long = lastSampleV_minusDC_long[phase] + + (((sampleV_minusDC_long - lastSampleV_minusDC_long[phase])*phaseCal_int[phase])>>8); + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleV_minusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP[phase] +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // for the Vrms calculation (for datalogging only) + long inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (now x4096, or 2^12) + inst_Vsquared = inst_Vsquared>>12; // scaling is now x1 (V_ADC x I_ADC) + sum_Vsquared[phase] += inst_Vsquared; // cumulative V^2 (V_ADC x I_ADC) + if (phase == 0) { + samplesDuringThisDatalogPeriod ++; } // no need to keep separate counts for each phase + + // general housekeeping + cumVdeltasThisCycle_long[phase] += sampleV_minusDC_long; // for use with LP filter + samplesDuringThismainsCycle[phase] ++; + + // store items for use during next loop + lastSampleV_minusDC_long[phase] = sampleV_minusDC_long; // required for phaseCal algorithm + polarityOfLastSampleV[phase] = polarityNow; // for identification of half cycle boundaries + } +} +// end of processRawSamples() + + +void processLatestContribution(byte phase, float power) +{ + float latestEnergyContribution = power; // for efficiency, the energy scale is Joules * 50 + + // add the latest energy contribution to the relevant per-phase accumulator + // (only used for datalogging of power) + energyStateOfPhase[phase] += latestEnergyContribution; + + // add the latest energy contribution to the main energy accumulator + energyInBucket_main += latestEnergyContribution; + + // apply any adjustment that is required. + if (phase == 0) + { + energyInBucket_main -= REQUIRED_EXPORT_IN_WATTS; // energy scale is Joules x 50 + } +// + // Apply max and min limits to the main accumulator's level + if (energyInBucket_main > energyBucketCapacity_main) { + energyInBucket_main = energyBucketCapacity_main; } + else + if (energyInBucket_main < 0) { + energyInBucket_main = 0; } +} + +void increaseLoadIfPossible() +{ + /* if permitted by A/F rules, turn on the highest priority logical load that is not already on. + */ + boolean changed = false; + + // Only one load may operate freely at a time. Other loads are prevented from + // switching until a sufficient period has elapsed since the last transition, and + // then only if the energy level has not have fallen since the previous transition. + // These measures allow a lower priority load to contribute if a higher priority + // load is not having the desired effect, but not immediately. + // + if (energyInBucket_main >= energyStateAtLastTransition) + { +// boolean timeout = (cycleCount > cycleCountAtLastTransition + interLoadSeparationDelay_cycles); + boolean timeout = (mainsCyclesSinceLastChangeOfLoadState > interLoadSeparationDelay_cycles); + for (int i = 0; i < noOfDumploads && !changed; i++) + { + if (logicalLoadState[i] == LOAD_OFF) + { + if ((i == activeLoadID) || timeout) + { + logicalLoadState[i] = LOAD_ON; + mainsCyclesSinceLastChangeOfLoadState = 0; + energyStateAtLastTransition = energyInBucket_main; + activeLoadID = i; + changed = true; + } + } + } + } + else + { + // energy level has not risen so there's no need to apply any more load + } +} + +void decreaseLoadIfPossible() +{ + /* if permitted by A/F rules, turn off the highest priority logical load that is not already off. + */ + boolean changed = false; + + // Only one load may operate freely at a time. Other loads are prevented from + // switching until a sufficient period has elapsed since the last transition, and + // then only if the energy level has not have risen since the previous transition. + // These measures allow a lower priority load to contribute if a higher priority + // load is not having the desired effect, but not immediately. + // + if (energyInBucket_main <= energyStateAtLastTransition) + { +// boolean timeout = (cycleCount > cycleCountAtLastTransition + interLoadSeparationDelay_cycles); + boolean timeout = (mainsCyclesSinceLastChangeOfLoadState > interLoadSeparationDelay_cycles); +// for (int i = 0; i < noOfDumploads && !done; i++) + for (int i = (noOfDumploads -1); i >= 0 && !changed; i--) + { + if (logicalLoadState[i] == LOAD_ON) + { + if ((i == activeLoadID) || timeout) + { + logicalLoadState[i] = LOAD_OFF; + mainsCyclesSinceLastChangeOfLoadState = 0; + energyStateAtLastTransition = energyInBucket_main; + activeLoadID = i; + changed = true; + } + } + } + } + else + { + // energy level has not fallen so there's no need to apply any more load + } +} + + + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * By default, the association between the physical and logical loads is 1:1. If + * physical load 1 is set to have priority, the logical-to-physical association for + * loads 0 and 1 are swapped. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } + + if (loadPriorityMode == SWITCHED_PRIORITIES) + { + // swap physical loads 0 & 1 if remote load has priority + physicalLoadState[0] = logicalLoadState[1]; + physicalLoadState[1] = logicalLoadState[0]; + } +} + + +// this function changes the value of the load priorities if the state of the external switch is altered +void checkLoadPrioritySelection() +{ + static byte loadPrioritySwitchCcount = 0; + int pinState = digitalRead(loadPrioritySelectorPin); + if (pinState != loadPriorityMode) + { + loadPrioritySwitchCcount++; + } + if (loadPrioritySwitchCcount >= 20) + { + loadPrioritySwitchCcount = 0; + loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable + Serial.print ("loadPriority selection changed to "); + if (loadPriorityMode == NORMAL_PRIORITIES) { + Serial.println ( "load 0"); } + else { + Serial.println ( "load 1"); } + } +} + + +// Although this sketch always operates in ANTI_FLICKER mode, it was convenient +// to leave this mechanism in place. +// +void configureParamsForSelectedOutputMode() +{ + if (outputMode == ANTI_FLICKER) + { + postMidPointCrossingDelay_cycles = postMidPointCrossingDelayForAF_cycles; + } + else + { + postMidPointCrossingDelay_cycles = 0; + } + + // display relevant settings for selected output mode + Serial.print(" energyBucketCapacity_main = "); + Serial.println(energyBucketCapacity_main); + Serial.print(" postMidPointCrossingDelay_cycles = "); + Serial.println(postMidPointCrossingDelay_cycles); + Serial.print(" interLoadSeparationDelay_cycles = "); + Serial.println(interLoadSeparationDelay_cycles); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + + +#ifdef RF_PRESENT +void send_rf_data() +{ + int i = 0; + while (!rf12_canSend() && i<10) + { + rf12_recvDone(); + i++; + } + rf12_sendStart(0, &tx_data, sizeof tx_data); +} +#endif + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_1a.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_1a.ino new file mode 100644 index 0000000..5b823f8 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_1a.ino @@ -0,0 +1,875 @@ +/* Mk2_3phase_RFdatalog_1 -> Mk2_3phase_RFdatalog_1a + * + * This sketch provides continuous monitoring of real power on three phases. + * Surplus power is diverted to multiple loads in sequential order. + * Datalogging of real power and Vrms is provided for each phase. + * The presence or absence of the RFM12B needs to be set at compile time + * + * 14-Aug-2015 renamed as version 1a + * - addition of mechanism to display the minimum number of sample sets per mains cycle; + * - to avoid data loss, datalog messages are no longer set to the Serial port. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + * August 2015 + */ + +#include // may not be needed, but it's probably a good idea to include this + +//#define RF_PRESENT // <- this line should be commented out if the RFM12B module is not present + +#ifdef RF_PRESENT +#include +#endif + +// In this sketch, the ADC is free-running with a cycle time of ~104uS. + +// WORKLOAD_CHECK is available for determining how much spare processing time there +// is. To activate this mode, the #define line below should be included: +//#define WORKLOAD_CHECK + +#define CYCLES_PER_SECOND 50 +#define JOULES_PER_WATT_HOUR 3600 // may be needed for datalogging +#define SWEETZONE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator +#define NO_OF_PHASES 3 +#define TRIAC_ON LOW +#define TRIAC_OFF HIGH +#define DATALOG_PERIOD_IN_SECONDS 2 + +const byte noOfDumploads = 3; // The logic expects a minimum of 2 dumploads, + // for local & remote loads, but neither has to + // be physically present. + +enum polarities {NEGATIVE, POSITIVE}; +enum outputModes {ANTI_FLICKER, NORMAL}; +enum loadPriorityModes {SWITCHED_PRIORITIES, NORMAL_PRIORITIES}; + +enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are active low +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +enum energyStates {LOWER_HALF, UPPER_HALF}; // for single threshold AF algorithm +enum energyStates energyStateNow; + +// For this multi-load version, the same mechanism has been retained but the +// output mode is hard-coded as below: +enum outputModes outputMode = ANTI_FLICKER; + +// In this multi-load version, the external switch is re-used to determine the load priority +enum loadPriorityModes loadPriorityMode; + +#ifdef RF_PRESENT +#define freq RF12_433MHZ // Use the freq to match the module you have. + +const int nodeID = 10; +const int networkGroup = 210; +const int UNO = 1; +#endif + +typedef struct { + int power_L1; + int power_L2; + int power_L3; + int Vrms_L1; + int Vrms_L2; + int Vrms_L3;} Tx_struct; // revised data for RF comms +Tx_struct tx_data; + + +// ----------- Pinout assignments ----------- +// +// digital pins: +const byte loadPrioritySelectorPin = 3; // // for 3-phase PCB +const byte physicalLoad_0_pin = 8; // for 3-phase PCB, Load #1 +const byte physicalLoad_1_pin = 7; // for 3-phase PCB, Load #2 +const byte physicalLoad_2_pin = 6; // for 3-phase PCB, Load #3 + +// analogue input pins +const byte sensorV[NO_OF_PHASES] = {0,2,4}; // for 3-phase PCB +const byte sensorI[NO_OF_PHASES] = {1,3,5}; // for 3-phase PCB + + +// -------------- general global variables ----------------- +// +// Some of these variables are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPeriod = false; // start-up delay, allows things to settle +byte initialDelay = 3; // in seconds, to allow time to open the Serial monitor +byte startUpPeriod = 3; // in seconds, to allow LP filter to settle + +long DCoffset_V_long[NO_OF_PHASES]; // <--- for LPF +long DCoffset_V_min; // <--- for LPF (min limit) +long DCoffset_V_max; // <--- for LPF (max limit) +int DCoffset_I_nom; // nominal mid-point value of ADC @ x1 scale + +int postMidPointCrossingDelay_cycles; // assigned in setup(), different for each output mode +const int postMidPointCrossingDelayForAF_cycles = 10; // in 20 ms counts +const int interLoadSeparationDelay_cycles = 25; // in 20 ms cycle counts +byte activeLoadID; // only one load may operate freely at a time. +int mainsCyclesSinceLastMidPointCrossing = 0; +int mainsCyclesSinceLastChangeOfLoadState = 0; +float energyStateAtLastTransition; + +// for interaction between the main processor and the ISR +volatile boolean dataReady = false; +int sampleV[NO_OF_PHASES]; +int sampleI[NO_OF_PHASES]; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int lowestNoOfSampleSetsPerMainsCycle; + +// Calibration values +//------------------- +// Three calibration values are used in this sketch: powerCal, phaseCal and voltageCal. +// With most hardware, the default values are likely to work fine without +// need for change. A compact explanation of each of these values now follows: + +// When calculating real power, which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal[NO_OF_PHASES] = {0.0435, 0.043, 0.043}; +//const float powerCal[NO_OF_PHASES] = {0.05}; + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// NB. Any tool which determines the optimal value of phaseCal must have a similar +// scheme for taking sample values as does this sketch! +// +const float phaseCal[NO_OF_PHASES] = {0.5, 0.5, 0.5}; // <- nominal values only + +// For datalogging purposes, voltageCal has been added too. Because the range of ADC values is +// similar to the actual range of volts, the optimal value for this cal factor is likely to be +// close to unity. +const float voltageCal[NO_OF_PHASES] = {1.03, 1.03, 1.03}; // compared with Fluke 77 meter + + +int phaseCal_int[NO_OF_PHASES]; // to avoid the need for floating-point maths + +float energyBucketCapacity_main; +float energyBucketCapacity_perPhase; +float energyInBucket_main; +float midPointOfMainEnergyBucket; +boolean energyLevelInUpperHalf; + +// per-phase items +float energyStateOfPhase[NO_OF_PHASES]; // an energy bucket per phase +float energyBucketLevel_perPhase_max; // for restraining the per-phase energy bucket levels +float energyBucketLevel_perPhase_min; // for restraining the per-phase energy bucket levels + +int datalogCountInMainsCycles; +const int maxDatalogCountInMainsCycles = DATALOG_PERIOD_IN_SECONDS * CYCLES_PER_SECOND; + + +void setup() +{ + delay (initialDelay * 1000); // allows time to open the Serial Monitor + + Serial.begin(9600); // initialize Serial interface + Serial.println(); + Serial.println(); + Serial.println(); + Serial.println("----------------------------------"); + Serial.println("Sketch ID: Mk2_3phase_RFdatalog_1.ino"); + + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for Load #1 + pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for Load #2 + pinMode(physicalLoad_2_pin, OUTPUT); // driver pin for Load #3 + + for(int i = 0; i< noOfDumploads; i++) + { + logicalLoadState[i] = LOAD_OFF; + physicalLoadState[i] = LOAD_OFF; + } + + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // update the local load's state. + digitalWrite(physicalLoad_1_pin, physicalLoadState[1]); // update the additional load state (inverse logic). + digitalWrite(physicalLoad_2_pin, physicalLoadState[2]); // update the additional load state (inverse logic). + + pinMode(loadPrioritySelectorPin, INPUT); + digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and + loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority mode + + for (byte phase = 0; phase < NO_OF_PHASES; phase++) + { + // When using integer maths, calibration values that have been supplied in + // floating point form need to be rescaled. + phaseCal_int[phase] = phaseCal[phase] * 256; // for integer maths + DCoffset_V_long[phase] = 512L * 256; // nominal mid-point value of ADC @ x256 scale + } + + // Define operating limits for the LP filters which identify DC offset in the voltage + // sample streams. By limiting the output range, these filters always should start up + // correctly. + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + DCoffset_I_nom = 512; // nominal mid-point value of ADC @ x1 scale + + // for the main energy bucket + energyBucketCapacity_main = (float)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND; + midPointOfMainEnergyBucket = energyBucketCapacity_main * 0.5; // for resetting flexible thresholds + energyInBucket_main = 0; + energyStateAtLastTransition = midPointOfMainEnergyBucket; + + // for the per-phase energy buckets + energyBucketCapacity_perPhase = (float)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND; + energyBucketLevel_perPhase_max = energyBucketCapacity_perPhase * 0.5; + energyBucketLevel_perPhase_min = energyBucketCapacity_perPhase * -0.5; + + Serial.println ("ADC mode: free-running"); + Serial.print ("requiredExport in Watts = "); + Serial.println (REQUIRED_EXPORT_IN_WATTS); + + // Set up the ADC to be free-running + ADCSRA = (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples (3 pairs). The main processor and +// the ADC work autonomously, their operation being only linked via the dataReady flag. +// As soon as data is made available by the ADC, the main processor can start to work +// on it immediately. +// +void processRawSamples() +{ + static long sumP[NO_OF_PHASES]; + static enum polarities polarityOfLastSampleV[NO_OF_PHASES]; // for zero-crossing detection + static long lastSampleV_minusDC_long[NO_OF_PHASES]; // for the phaseCal algorithm + static long cumVdeltasThisCycle_long[NO_OF_PHASES]; // for the LPF which determines DC offset (voltage) + static int samplesDuringThismainsCycle[NO_OF_PHASES]; + static long sum_Vsquared[NO_OF_PHASES]; + static long samplesDuringThisDatalogPeriod; + enum polarities polarityNow; + float realPower; + + // The raw V and I samples are processed in "phase pairs" + for (byte phase = 0; phase < NO_OF_PHASES; phase++) + { + // remove DC offset from each raw voltage sample by subtracting the accurate value + // as determined by its associated LP filter. + long sampleV_minusDC_long = ((long)sampleV[phase]<<8) - DCoffset_V_long[phase]; + + // determine polarity, to aid the logical flow + if(sampleV_minusDC_long > 0) { + polarityNow = POSITIVE; } + else { + polarityNow = NEGATIVE; } + + if (polarityNow == POSITIVE) + { + if (beyondStartUpPeriod) + { + if (polarityOfLastSampleV[phase] != POSITIVE) + { + /* This is the start of a new +ve half cycle, for this phase, just after the + * zero-crossing point. Before the contribution from this phase can be added + * to the running total, the cal factor for this phase must be applied. + */ + realPower = (float)(sumP[phase] / samplesDuringThismainsCycle[phase]) * powerCal[phase]; + + processLatestContribution(phase, realPower); // runs at 6.6 ms intervals + + if (phase == 0) + { + // continuity checker + if (samplesDuringThismainsCycle[0] < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = samplesDuringThismainsCycle[0]; } + + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + } + + sumP[phase] = 0; + samplesDuringThismainsCycle[phase] = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + if ((phase == 0) && samplesDuringThismainsCycle[0] == 5) + { + // This code is executed once per 20mS, shortly after the start of each new + // mains cycle on phase 0. + // +// cycleCount++; + mainsCyclesSinceLastMidPointCrossing++; + mainsCyclesSinceLastChangeOfLoadState++; + datalogCountInMainsCycles++; + + /* Now it's time to determine whether any of the the loads need to be changed. + * This is a 2-stage process: + * First, change the LOGICAL loads as necessary, then update the PHYSICAL + * loads according to the mapping that exists between them. The mapping is + * 1:1 by default but can be altered by a hardware switch which allows the + * priorities of the first two load to be swapped. + * This code uses a single-threshold algorithm which relies on regular switching + * of the load to maintain the energy balance within the permitted range. + */ + + // when using the single-threshold power diversion algorithm, a counter needs + // to be reset whenever the energy level in the accumulator crosses the mid-point + // + enum energyStates energyStateOnLastLoop = energyStateNow; + + if (energyInBucket_main > midPointOfMainEnergyBucket) { + energyStateNow = UPPER_HALF; } + else { + energyStateNow = LOWER_HALF; } + + if (energyStateNow != energyStateOnLastLoop) { + mainsCyclesSinceLastMidPointCrossing = 0; } + + + if (mainsCyclesSinceLastMidPointCrossing > postMidPointCrossingDelay_cycles) + { + if (energyStateNow == UPPER_HALF) + { + increaseLoadIfPossible(); // to reduce the level in the bucket + } + else + { + decreaseLoadIfPossible(); // to increase the level in the bucket + } + } + + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + // update all the physical loads + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger + digitalWrite(physicalLoad_1_pin, physicalLoadState[1]); // active low for trigger + digitalWrite(physicalLoad_2_pin, physicalLoadState[2]); // active low for trigger + + if (datalogCountInMainsCycles >= maxDatalogCountInMainsCycles) + { + datalogCountInMainsCycles = 0; + + tx_data.power_L1 = energyStateOfPhase[0] / maxDatalogCountInMainsCycles; + tx_data.power_L2 = energyStateOfPhase[1] / maxDatalogCountInMainsCycles; + tx_data.power_L3 = energyStateOfPhase[2] / maxDatalogCountInMainsCycles; + tx_data.Vrms_L1 = (int)(voltageCal[0] * sqrt(sum_Vsquared[0] / samplesDuringThisDatalogPeriod)); + tx_data.Vrms_L2 = (int)(voltageCal[1] * sqrt(sum_Vsquared[1] / samplesDuringThisDatalogPeriod)); + tx_data.Vrms_L3 = (int)(voltageCal[2] * sqrt(sum_Vsquared[2] / samplesDuringThisDatalogPeriod)); +#ifdef RF_PRESENT + send_rf_data(); +#endif +/* + Serial.print(energyInBucket_main / CYCLES_PER_SECOND); + Serial.print(", "); + Serial.print(tx_data.power_L1); + Serial.print(", "); + Serial.print(tx_data.power_L2); + Serial.print(", "); + Serial.print(tx_data.power_L3); + Serial.print(", "); + Serial.print(tx_data.Vrms_L1); + Serial.print(", "); + Serial.print(tx_data.Vrms_L2); + Serial.print(", "); + Serial.println(tx_data.Vrms_L3); + */ + energyStateOfPhase[0] = 0; + energyStateOfPhase[1] = 0; + energyStateOfPhase[2] = 0; + sum_Vsquared[0] = 0; + sum_Vsquared[1] = 0; + sum_Vsquared[2] = 0; + samplesDuringThisDatalogPeriod = 0; + } + } + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (initialDelay + startUpPeriod) * 1000) + { + beyondStartUpPeriod = true; + Serial.println ("Go!"); + } + } + } // end of processing that is specific to samples where the voltage is positive + + else // the polarity of this sample is negative + { + if (polarityOfLastSampleV[phase] != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // This is a convenient point to update the Low Pass Filter for the phase that is + // being processed. This needs to be done right from the start. + // + DCoffset_V_long[phase] += (cumVdeltasThisCycle_long[phase]>>6); // faster than * 0.01 + cumVdeltasThisCycle_long[phase] = 0; + + // To ensure that this LP filter will always start up correctly when 240V AC is + // available, its output value needs to be prevented from drifting beyond the likely range + // of the voltage signal. + // + if (DCoffset_V_long[phase] < DCoffset_V_min) { + DCoffset_V_long[phase] = DCoffset_V_min; } + else + if (DCoffset_V_long[phase] > DCoffset_V_max) { + DCoffset_V_long[phase] = DCoffset_V_max; } + + checkLoadPrioritySelection(); // updates load priorities if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is negitive + + // Processing for EVERY pair of samples. Most of this code is not used during the + // start-up period, but it does no harm to leave it in place. Accumulated values + // are cleared when beyondStartUpPhase is set to true. + // + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_long = ((long)(sampleI[phase] - DCoffset_I_nom))<<8; + + // phase-shift the voltage waveform so that it aligns with the current when a + // resistive load is used + long phaseShiftedSampleV_minusDC_long = lastSampleV_minusDC_long[phase] + + (((sampleV_minusDC_long - lastSampleV_minusDC_long[phase])*phaseCal_int[phase])>>8); + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleV_minusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP[phase] +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // for the Vrms calculation (for datalogging only) + long inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (now x4096, or 2^12) + inst_Vsquared = inst_Vsquared>>12; // scaling is now x1 (V_ADC x I_ADC) + sum_Vsquared[phase] += inst_Vsquared; // cumulative V^2 (V_ADC x I_ADC) + if (phase == 0) { + samplesDuringThisDatalogPeriod ++; } // no need to keep separate counts for each phase + + // general housekeeping + cumVdeltasThisCycle_long[phase] += sampleV_minusDC_long; // for use with LP filter + samplesDuringThismainsCycle[phase] ++; + + // store items for use during next loop + lastSampleV_minusDC_long[phase] = sampleV_minusDC_long; // required for phaseCal algorithm + polarityOfLastSampleV[phase] = polarityNow; // for identification of half cycle boundaries + } +} +// end of processRawSamples() + + +void processLatestContribution(byte phase, float power) +{ + float latestEnergyContribution = power; // for efficiency, the energy scale is Joules * 50 + + // add the latest energy contribution to the relevant per-phase accumulator + // (only used for datalogging of power) + energyStateOfPhase[phase] += latestEnergyContribution; + + // add the latest energy contribution to the main energy accumulator + energyInBucket_main += latestEnergyContribution; + + // apply any adjustment that is required. + if (phase == 0) + { + energyInBucket_main -= REQUIRED_EXPORT_IN_WATTS; // energy scale is Joules x 50 + } +// + // Apply max and min limits to the main accumulator's level + if (energyInBucket_main > energyBucketCapacity_main) { + energyInBucket_main = energyBucketCapacity_main; } + else + if (energyInBucket_main < 0) { + energyInBucket_main = 0; } +} + +void increaseLoadIfPossible() +{ + /* if permitted by A/F rules, turn on the highest priority logical load that is not already on. + */ + boolean changed = false; + + // Only one load may operate freely at a time. Other loads are prevented from + // switching until a sufficient period has elapsed since the last transition, and + // then only if the energy level has not have fallen since the previous transition. + // These measures allow a lower priority load to contribute if a higher priority + // load is not having the desired effect, but not immediately. + // + if (energyInBucket_main >= energyStateAtLastTransition) + { +// boolean timeout = (cycleCount > cycleCountAtLastTransition + interLoadSeparationDelay_cycles); + boolean timeout = (mainsCyclesSinceLastChangeOfLoadState > interLoadSeparationDelay_cycles); + for (int i = 0; i < noOfDumploads && !changed; i++) + { + if (logicalLoadState[i] == LOAD_OFF) + { + if ((i == activeLoadID) || timeout) + { + logicalLoadState[i] = LOAD_ON; + mainsCyclesSinceLastChangeOfLoadState = 0; + energyStateAtLastTransition = energyInBucket_main; + activeLoadID = i; + changed = true; + } + } + } + } + else + { + // energy level has not risen so there's no need to apply any more load + } +} + +void decreaseLoadIfPossible() +{ + /* if permitted by A/F rules, turn off the highest priority logical load that is not already off. + */ + boolean changed = false; + + // Only one load may operate freely at a time. Other loads are prevented from + // switching until a sufficient period has elapsed since the last transition, and + // then only if the energy level has not have risen since the previous transition. + // These measures allow a lower priority load to contribute if a higher priority + // load is not having the desired effect, but not immediately. + // + if (energyInBucket_main <= energyStateAtLastTransition) + { +// boolean timeout = (cycleCount > cycleCountAtLastTransition + interLoadSeparationDelay_cycles); + boolean timeout = (mainsCyclesSinceLastChangeOfLoadState > interLoadSeparationDelay_cycles); +// for (int i = 0; i < noOfDumploads && !done; i++) + for (int i = (noOfDumploads -1); i >= 0 && !changed; i--) + { + if (logicalLoadState[i] == LOAD_ON) + { + if ((i == activeLoadID) || timeout) + { + logicalLoadState[i] = LOAD_OFF; + mainsCyclesSinceLastChangeOfLoadState = 0; + energyStateAtLastTransition = energyInBucket_main; + activeLoadID = i; + changed = true; + } + } + } + } + else + { + // energy level has not fallen so there's no need to apply any more load + } +} + + + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * By default, the association between the physical and logical loads is 1:1. If + * physical load 1 is set to have priority, the logical-to-physical association for + * loads 0 and 1 are swapped. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } + + if (loadPriorityMode == SWITCHED_PRIORITIES) + { + // swap physical loads 0 & 1 if remote load has priority + physicalLoadState[0] = logicalLoadState[1]; + physicalLoadState[1] = logicalLoadState[0]; + } +} + + +// this function changes the value of the load priorities if the state of the external switch is altered +void checkLoadPrioritySelection() +{ + static byte loadPrioritySwitchCcount = 0; + int pinState = digitalRead(loadPrioritySelectorPin); + if (pinState != loadPriorityMode) + { + loadPrioritySwitchCcount++; + } + if (loadPrioritySwitchCcount >= 20) + { + loadPrioritySwitchCcount = 0; + loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable + Serial.print ("loadPriority selection changed to "); + if (loadPriorityMode == NORMAL_PRIORITIES) { + Serial.println ( "load 0"); } + else { + Serial.println ( "load 1"); } + } +} + + +// Although this sketch always operates in ANTI_FLICKER mode, it was convenient +// to leave this mechanism in place. +// +void configureParamsForSelectedOutputMode() +{ + if (outputMode == ANTI_FLICKER) + { + postMidPointCrossingDelay_cycles = postMidPointCrossingDelayForAF_cycles; + } + else + { + postMidPointCrossingDelay_cycles = 0; + } + + // display relevant settings for selected output mode + Serial.print(" energyBucketCapacity_main = "); + Serial.println(energyBucketCapacity_main); + Serial.print(" postMidPointCrossingDelay_cycles = "); + Serial.println(postMidPointCrossingDelay_cycles); + Serial.print(" interLoadSeparationDelay_cycles = "); + Serial.println(interLoadSeparationDelay_cycles); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + + +#ifdef RF_PRESENT +void send_rf_data() +{ + int i = 0; + while (!rf12_canSend() && i<10) + { + rf12_recvDone(); + i++; + } + rf12_sendStart(0, &tx_data, sizeof tx_data); +} +#endif + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_2.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_2.ino new file mode 100644 index 0000000..720d335 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_2.ino @@ -0,0 +1,941 @@ +/* Mk2_3phase_RFdatalog_2.ino + * + * Issue 1 was released in January 2015. + * + * This sketch provides continuous monitoring of real power on three phases. + * Surplus power is diverted to multiple loads in sequential order. + * Datalogging of real power and Vrms is provided for each phase. + * The presence or absence of the RFM12B needs to be set at compile time + * + * Jan 2016, renamed as Mk2_3phase_RFdatalog_2 with these changes: + * - Improved control of multiple loads has been imported from the + * equivalent 1-phase sketch, Mk2_multiLoad_wired_6.ino + * - the ISR has been upgraded to fix a possible timing anomaly + * - variables to store ADC samples are now declared as "volatile" + * - for RF69 RF module is now supported + * - a performance check has been added with the result being sent to the Serial port + * - control signals for loads are now active-high to suit the latest 3-phase PCB + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include // may not be needed, but it's probably a good idea to include this + +// #define RF_PRESENT // <- this line should be commented out if the RFM12B module is not present + +#ifdef RF_PRESENT +//#define RF69_COMPAT 0 // for the RFM12B +#define RF69_COMPAT 1 // for the RF69 +#include +#endif + +// In this sketch, the ADC is free-running with a cycle time of ~104uS. + +// WORKLOAD_CHECK is available for determining how much spare processing time there +// is. To activate this mode, the #define line below should be included: +//#define WORKLOAD_CHECK + +#define CYCLES_PER_SECOND 50 +//#define JOULES_PER_WATT_HOUR 3600 // may be needed for datalogging +#define WORKING_ZONE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator +#define NO_OF_PHASES 3 +#define DATALOG_PERIOD_IN_SECONDS 5 + +const byte noOfDumploads = 3; + +enum polarities {NEGATIVE, POSITIVE}; +enum outputModes {ANTI_FLICKER, NORMAL}; +enum loadPriorityModes {LOAD_1_HAS_PRIORITY, LOAD_0_HAS_PRIORITY}; + +// enum loadStates {LOAD_ON, LOAD_OFF}; // for use if loads are active low (original PCB) +enum loadStates {LOAD_OFF, LOAD_ON}; // for use if loads are active high (Rev 2 PCB) +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +// For this multi-load version, the same mechanism has been retained but the +// output mode is hard-coded as below: +enum outputModes outputMode = ANTI_FLICKER; + +// In this multi-load version, the external switch is re-used to determine the load priority +enum loadPriorityModes loadPriorityMode = LOAD_0_HAS_PRIORITY; + +#ifdef RF_PRESENT +#define freq RF12_433MHZ // Use the freq to match the module you have. + +const int nodeID = 10; +const int networkGroup = 210; +const int UNO = 1; +#endif + +typedef struct { + int power_L1; + int power_L2; + int power_L3; + int Vrms_L1; + int Vrms_L2; + int Vrms_L3;} Tx_struct; // revised data for RF comms +Tx_struct tx_data; + + +// ----------- Pinout assignments ----------- +// +// digital pins: +const byte loadPrioritySelectorPin = 3; // // for 3-phase PCB +// D4 is not in use +const byte physicalLoad_0_pin = 5; // for 3-phase PCB, Load #1 (Rev 2 PCB) +const byte physicalLoad_1_pin = 6; // for 3-phase PCB, Load #2 (Rev 2 PCB) +const byte physicalLoad_2_pin = 7; // for 3-phase PCB, Load #3 (Rev 2 PCB) +// D8 is not in use +// D9 is not in use + +// analogue input pins +const byte sensorV[NO_OF_PHASES] = {0,2,4}; // for 3-phase PCB +const byte sensorI[NO_OF_PHASES] = {1,3,5}; // for 3-phase PCB + + +// -------------- general global variables ----------------- +// +// Some of these variables are used in multiple blocks so cannot be static. +// For integer maths, some variables need to be 'long' +// +boolean beyondStartUpPeriod = false; // start-up delay, allows things to settle +byte initialDelay = 3; // in seconds, to allow time to open the Serial monitor +byte startUpPeriod = 3; // in seconds, to allow LP filter to settle + +long DCoffset_V_long[NO_OF_PHASES]; // <--- for LPF +long DCoffset_V_min; // <--- for LPF (min limit) +long DCoffset_V_max; // <--- for LPF (max limit) +int DCoffset_I_nom; // nominal mid-point value of ADC @ x1 scale + +// for 3-phase use, with units of Joules * CYCLES_PER_SECOND +float capacityOfEnergyBucket_main; +float energyInBucket_main; +float midPointOfEnergyBucket_main; +float lowerThreshold_default; +float lowerEnergyThreshold; +float upperThreshold_default; +float upperEnergyThreshold; +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.4 + +// for improved control of multiple loads +boolean recentTransition = false; +byte postTransitionCount; +#define POST_TRANSITION_MAX_COUNT 3 // <-- allows each transition to take effect +//#define POST_TRANSITION_MAX_COUNT 50 // <-- for testing only +byte activeLoad = 0; + +// for datalogging +int datalogCountInMainsCycles; +const int maxDatalogCountInMainsCycles = DATALOG_PERIOD_IN_SECONDS * CYCLES_PER_SECOND; +float energyStateOfPhase[NO_OF_PHASES]; // only used for datalogging + +// for interaction between the main processor and the ISR +volatile boolean dataReady = false; +volatile int sampleV[NO_OF_PHASES]; +volatile int sampleI[NO_OF_PHASES]; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int mainsCycles_forContinuityChecker; +int lowestNoOfSampleSetsPerMainsCycle; + +// Calibration values +//------------------- +// Three calibration values are used in this sketch: powerCal, phaseCal and voltageCal. +// With most hardware, the default values are likely to work fine without +// need for change. A compact explanation of each of these values now follows: + +// When calculating real power, which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal[NO_OF_PHASES] = {0.043, 0.043, 0.043}; + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// NB. Any tool which determines the optimal value of phaseCal must have a similar +// scheme for taking sample values as does this sketch. +// +const float phaseCal[NO_OF_PHASES] = {0.5, 0.5, 0.5}; // <- nominal values only +int phaseCal_int[NO_OF_PHASES]; // to avoid the need for floating-point maths + +// For datalogging purposes, voltageCal has been added too. Because the range of ADC values is +// similar to the actual range of volts, the optimal value for this cal factor is likely to be +// close to unity. +const float voltageCal[NO_OF_PHASES] = {1.03, 1.03, 1.03}; // compared with Fluke 77 meter + + + + +void setup() +{ + delay (initialDelay * 1000); // allows time to open the Serial Monitor + + Serial.begin(9600); // initialize Serial interface + Serial.println(); + Serial.println(); + Serial.println(); + Serial.println("----------------------------------"); + Serial.println("Sketch ID: Mk2_3phase_RFdatalog_2.ino"); + + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for Load #1 + pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for Load #2 + pinMode(physicalLoad_2_pin, OUTPUT); // driver pin for Load #3 + + for(int i = 0; i< noOfDumploads; i++) + { + logicalLoadState[i] = LOAD_OFF; + } + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // update the local load's state. + digitalWrite(physicalLoad_1_pin, physicalLoadState[1]); // update the additional load state (inverse logic). + digitalWrite(physicalLoad_2_pin, physicalLoadState[2]); // update the additional load state (inverse logic). + + pinMode(loadPrioritySelectorPin, INPUT); + digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and + loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority mode + + for (byte phase = 0; phase < NO_OF_PHASES; phase++) + { + // When using integer maths, calibration values that have been supplied in + // floating point form need to be rescaled. + phaseCal_int[phase] = phaseCal[phase] * 256; // for integer maths + DCoffset_V_long[phase] = 512L * 256; // nominal mid-point value of ADC @ x256 scale + } + + // Define operating limits for the LP filters which identify DC offset in the voltage + // sample streams. By limiting the output range, these filters always should start up + // correctly. + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + DCoffset_I_nom = 512; // nominal mid-point value of ADC @ x1 scale + + // for the main energy bucket + capacityOfEnergyBucket_main = (float)WORKING_ZONE_IN_JOULES * CYCLES_PER_SECOND; + midPointOfEnergyBucket_main = capacityOfEnergyBucket_main * 0.5; // for resetting flexible thresholds + energyInBucket_main = 0; + + Serial.println ("ADC mode: free-running"); + Serial.print ("requiredExport in Watts = "); + Serial.println (REQUIRED_EXPORT_IN_WATTS); + + // Set up the ADC to be free-running + ADCSRA = (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples (3 pairs). The main processor and +// the ADC work autonomously, their operation being synchnonised only via the dataReady flag. +// +void processRawSamples() +{ + static long sumP[NO_OF_PHASES]; + static enum polarities polarityOfLastSampleV[NO_OF_PHASES]; // for zero-crossing detection + static long lastSampleV_minusDC_long[NO_OF_PHASES]; // for the phaseCal algorithm + static long cumVdeltasThisCycle_long[NO_OF_PHASES]; // for the LPF which determines DC offset (voltage) + static int samplesDuringThisMainsCycle[NO_OF_PHASES]; + static long sum_Vsquared[NO_OF_PHASES]; + static long samplesDuringThisDatalogPeriod; + enum polarities polarityNow; + + // The raw V and I samples are processed in "phase pairs" + for (byte phase = 0; phase < NO_OF_PHASES; phase++) + { + // remove DC offset from each raw voltage sample by subtracting the accurate value + // as determined by its associated LP filter. + long sampleV_minusDC_long = ((long)sampleV[phase]<<8) - DCoffset_V_long[phase]; + + // determine polarity, to aid the logical flow + if(sampleV_minusDC_long > 0) { + polarityNow = POSITIVE; } + else { + polarityNow = NEGATIVE; } + + if (polarityNow == POSITIVE) + { + if (beyondStartUpPeriod) + { + if (polarityOfLastSampleV[phase] != POSITIVE) + { + // This is the start of a new +ve half cycle, for this phase, just after the + // zero-crossing point. Before the contribution from this phase can be added + // to the running total, the cal factor for this phase must be applied. + // + float realPower = (sumP[phase] / samplesDuringThisMainsCycle[phase]) * powerCal[phase]; + + processLatestContribution(phase, realPower); // runs at 6.6 ms intervals + + // A performance check to monitor and display the minimum number of sets of + // ADC samples per mains cycle, the expected number being 20ms / (104us * 6) = 32.05 + // + if (phase == 0) + { + if (samplesDuringThisMainsCycle[phase] < lowestNoOfSampleSetsPerMainsCycle) + { + lowestNoOfSampleSetsPerMainsCycle = samplesDuringThisMainsCycle[phase]; + } + mainsCycles_forContinuityChecker++; + if (mainsCycles_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + mainsCycles_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + } + + sumP[phase] = 0; + samplesDuringThisMainsCycle[phase] = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + if ((phase == 0) && samplesDuringThisMainsCycle[0] == 2) // lower value for larger sample set + { + // This code is executed once per 20mS, shortly after the start of each new + // mains cycle on phase 0. + // + datalogCountInMainsCycles++; + + // Changling the state of the loads is is a 3-part process: + // - change the LOGICAL load states as necessary to maintain the energy level + // - update the PHYSICAL load states according to the logical -> physical mapping + // - update the driver lines for each of the loads. + // + // Restrictions apply for the period immediately after a load has been switched. + // Here the recentTransition flag is checked and updated as necessary. + if (recentTransition) + { + postTransitionCount++; + if (postTransitionCount >= POST_TRANSITION_MAX_COUNT) + { + recentTransition = false; + } + } + + if (energyInBucket_main > midPointOfEnergyBucket_main) + { + // the energy state is in the upper half of the working range + lowerEnergyThreshold = lowerThreshold_default; // reset the "opposite" threshold + if (energyInBucket_main > upperEnergyThreshold) + { + // Because the energy level is high, some action may be required + boolean OK_toAddLoad = true; + byte tempLoad = nextLogicalLoadToBeAdded(); + if (tempLoad < noOfDumploads) + { + // a load which is now OFF has been identified for potentially being switched ON + if (recentTransition) + { + // During the post-transition period, any increase in the energy level is noted. + if (energyInBucket_main > upperEnergyThreshold) + { + upperEnergyThreshold = energyInBucket_main; + + // the energy thresholds must remain within range + if (upperEnergyThreshold > capacityOfEnergyBucket_main) + { + upperEnergyThreshold = capacityOfEnergyBucket_main; + } + } + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toAddLoad = false; + } + } + + if (OK_toAddLoad) + { + logicalLoadState[tempLoad] = LOAD_ON; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + Serial.print('+'); + Serial.println(activeLoad); + } + } + } + } + else + { // the energy state is in the lower half of the working range + upperEnergyThreshold = upperThreshold_default; // reset the "opposite" threshold + if (energyInBucket_main < lowerEnergyThreshold) + { + // Because the energy level is low, some action may be required + boolean OK_toRemoveLoad = true; + byte tempLoad = nextLogicalLoadToBeRemoved(); + if (tempLoad < noOfDumploads) + { + // a load which is now ON has been identified for potentially being switched OFF + if (recentTransition) + { + // During the post-transition period, any decrease in the energy level is noted. + if (energyInBucket_main < lowerEnergyThreshold) + { + lowerEnergyThreshold = energyInBucket_main; + + // the energy thresholds must remain within range + if (lowerEnergyThreshold < 0) + { + lowerEnergyThreshold = 0; + } + } + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toRemoveLoad = false; + } + } + + if (OK_toRemoveLoad) + { + logicalLoadState[tempLoad] = LOAD_OFF; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + Serial.print('-'); + Serial.println(activeLoad); + } + } + } + } + + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + // update the control ports for each of the physical loads + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger + digitalWrite(physicalLoad_1_pin, physicalLoadState[1]); // active low for trigger + digitalWrite(physicalLoad_2_pin, physicalLoadState[2]); // active low for trigger + + // Now that the energy-related decisions have been taken, min and max limits can now + // be applied to the level of the energy bucket. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_main > capacityOfEnergyBucket_main) { + energyInBucket_main = capacityOfEnergyBucket_main; } + else + if (energyInBucket_main < 0) { + energyInBucket_main = 0; } + + if (datalogCountInMainsCycles >= maxDatalogCountInMainsCycles) + { + datalogCountInMainsCycles = 0; + + tx_data.power_L1 = energyStateOfPhase[0] / maxDatalogCountInMainsCycles; + tx_data.power_L2 = energyStateOfPhase[1] / maxDatalogCountInMainsCycles; + tx_data.power_L3 = energyStateOfPhase[2] / maxDatalogCountInMainsCycles; + tx_data.Vrms_L1 = (int)(voltageCal[0] * sqrt(sum_Vsquared[0] / samplesDuringThisDatalogPeriod)); + tx_data.Vrms_L2 = (int)(voltageCal[1] * sqrt(sum_Vsquared[1] / samplesDuringThisDatalogPeriod)); + tx_data.Vrms_L3 = (int)(voltageCal[2] * sqrt(sum_Vsquared[2] / samplesDuringThisDatalogPeriod)); +#ifdef RF_PRESENT + send_rf_data(); +#endif +/* <-- Warning - Unlike its 1-phase equivalent, this 3-phase code can be affected by Serial statements! + Serial.print(energyInBucket_main / CYCLES_PER_SECOND); + Serial.print(", "); +// + Serial.print(tx_data.power_L1); + Serial.print(", "); + Serial.print(tx_data.power_L2); + Serial.print(", "); + Serial.print(tx_data.power_L3); + Serial.print(", "); + Serial.print(tx_data.Vrms_L1); + Serial.print(", "); + Serial.print(tx_data.Vrms_L2); + Serial.print(", "); + Serial.println(tx_data.Vrms_L3); +*/ + energyStateOfPhase[0] = 0; + energyStateOfPhase[1] = 0; + energyStateOfPhase[2] = 0; + sum_Vsquared[0] = 0; + sum_Vsquared[1] = 0; + sum_Vsquared[2] = 0; + samplesDuringThisDatalogPeriod = 0; + + +/* <-- Warning - Unlike its 1-phase equivalent, this 3-phase code can be affected by Serial statements! + for (int i = 0; i < noOfDumploads; i++) + { + Serial.print(logicalLoadState[i]); + } + Serial.println(); +*/ + } + } + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (initialDelay + startUpPeriod) * 1000) + { + beyondStartUpPeriod = true; + mainsCycles_forContinuityChecker = 0; + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + } // end of processing that is specific to samples where the voltage is positive + + else // the polarity of this sample is negative + { + if (polarityOfLastSampleV[phase] != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // This is a convenient point to update the Low Pass Filter for the phase that is + // being processed. This needs to be done right from the start. + // + DCoffset_V_long[phase] += (cumVdeltasThisCycle_long[phase]>>6); // faster than * 0.01 + cumVdeltasThisCycle_long[phase] = 0; + + // To ensure that this LP filter will always start up correctly when 240V AC is + // available, its output value needs to be prevented from drifting beyond the likely range + // of the voltage signal. + // + if (DCoffset_V_long[phase] < DCoffset_V_min) { + DCoffset_V_long[phase] = DCoffset_V_min; } + else + if (DCoffset_V_long[phase] > DCoffset_V_max) { + DCoffset_V_long[phase] = DCoffset_V_max; } + + if (phase == 0) + { + checkLoadPrioritySelection(); // updates load priorities if the switch is changed + } + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is negitive + + // Processing for EVERY pair of samples. Most of this code is not used during the + // start-up period, but it does no harm to leave it in place. Accumulated values + // are cleared when the beyondStartUpPhase flag is set to true. + // + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_long = ((long)(sampleI[phase] - DCoffset_I_nom))<<8; + + // phase-shift the voltage waveform so that it aligns with the current when a + // resistive load is used + long phaseShiftedSampleV_minusDC_long = lastSampleV_minusDC_long[phase] + + (((sampleV_minusDC_long - lastSampleV_minusDC_long[phase])*phaseCal_int[phase])>>8); + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleV_minusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP[phase] +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // for the Vrms calculation (for datalogging only) + long inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (now x4096, or 2^12) + inst_Vsquared = inst_Vsquared>>12; // scaling is now x1 (V_ADC x I_ADC) + sum_Vsquared[phase] += inst_Vsquared; // cumulative V^2 (V_ADC x I_ADC) + if (phase == 0) { + samplesDuringThisDatalogPeriod ++; } // no need to keep separate counts for each phase + + // general housekeeping + cumVdeltasThisCycle_long[phase] += sampleV_minusDC_long; // for use with LP filter + samplesDuringThisMainsCycle[phase] ++; + + // store items for use during next loop + lastSampleV_minusDC_long[phase] = sampleV_minusDC_long; // required for phaseCal algorithm + polarityOfLastSampleV[phase] = polarityNow; // for identification of half cycle boundaries + } +} +// end of processRawSamples() + + +void processLatestContribution(byte phase, float power) +{ + float latestEnergyContribution = power; // for efficiency, the energy scale is Joules * CYCLES_PER_SECOND + + // add the latest energy contribution to the relevant per-phase accumulator + // (only used for datalogging of power) + energyStateOfPhase[phase] += latestEnergyContribution; + + // add the latest energy contribution to the main energy accumulator + energyInBucket_main += latestEnergyContribution; + + // apply any adjustment that is required. + if (phase == 0) + { + energyInBucket_main -= REQUIRED_EXPORT_IN_WATTS; // energy scale is Joules x 50 + } + + // Applying max and min limits to the main accumulator's level + // is deferred until after the energy related decisions have been taken + // +} + +byte nextLogicalLoadToBeAdded() +{ + byte retVal = noOfDumploads; + boolean success = false; + + for (byte index = 0; index < noOfDumploads && !success; index++) + { + if (logicalLoadState[index] == LOAD_OFF) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +byte nextLogicalLoadToBeRemoved() +{ + byte retVal = noOfDumploads; + boolean success = false; + + // NB. the index cannot be a 'byte' because the loop would not terminate correctly! + for (char index = (noOfDumploads -1); index >= 0 && !success; index--) + { + if (logicalLoadState[index] == LOAD_ON) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * The association between the physical and logical loads is 1:1. By default, numerical + * equivalence is maintained, so logical(N) maps to physical(N). If physical load 1 is set + * to have priority, rather than physical load 0, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * Any other mapping relaionships could be configured here. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } + + if (loadPriorityMode == LOAD_1_HAS_PRIORITY) + { + // swap physical loads 0 & 1 if remote load has priority + physicalLoadState[0] = logicalLoadState[1]; + physicalLoadState[1] = logicalLoadState[0]; + } +} + + +// this function changes the value of the load priorities if the state of the external switch is altered +void checkLoadPrioritySelection() +{ + static byte loadPrioritySwitchCcount = 0; + int pinState = digitalRead(loadPrioritySelectorPin); + if (pinState != loadPriorityMode) + { + loadPrioritySwitchCcount++; + } + if (loadPrioritySwitchCcount >= 20) + { + loadPrioritySwitchCcount = 0; + loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable + Serial.print ("loadPriority selection changed to "); + if (loadPriorityMode == LOAD_0_HAS_PRIORITY) { + Serial.println ( "load 0"); } + else { + Serial.println ( "load 1"); } + } +} + + +// Although this sketch always operates in ANTI_FLICKER mode, it was convenient +// to leave this mechanism in place. +// +void configureParamsForSelectedOutputMode() +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerThreshold_default = + capacityOfEnergyBucket_main * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperThreshold_default = + capacityOfEnergyBucket_main * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerThreshold_default = capacityOfEnergyBucket_main * 0.5; + upperThreshold_default = capacityOfEnergyBucket_main * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_main = "); + Serial.println(capacityOfEnergyBucket_main); + Serial.print(" lowerEnergyThreshold = "); + Serial.println(lowerThreshold_default); + Serial.print(" upperEnergyThreshold = "); + Serial.println(upperThreshold_default); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + + +#ifdef RF_PRESENT +void send_rf_data() +{ + int i = 0; + while (!rf12_canSend() && i<10) + { + rf12_recvDone(); + i++; + } + rf12_sendStart(0, &tx_data, sizeof tx_data); +} +#endif + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_3.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_3.ino new file mode 100644 index 0000000..d80279f --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_3.ino @@ -0,0 +1,966 @@ +/* Mk2_3phase_RFdatalog_3.ino + * + * Issue 1 was released in January 2015. + * + * This sketch provides continuous monitoring of real power on three phases. + * Surplus power is diverted to multiple loads in sequential order. A suitable + * output-stage is required for each load; this can be either triac-based, or a + * Solid State Relay. + * + * Datalogging of real power and Vrms is provided for each phase. + * The presence or absence of the RFM12B needs to be set at compile time + * + * January 2016, renamed as Mk2_3phase_RFdatalog_2 with these changes: + * - Improved control of multiple loads has been imported from the + * equivalent 1-phase sketch, Mk2_multiLoad_wired_6.ino + * - the ISR has been upgraded to fix a possible timing anomaly + * - variables to store ADC samples are now declared as "volatile" + * - for RF69 RF module is now supported + * - a performance check has been added with the result being sent to the Serial port + * - control signals for loads are now active-high to suit the latest 3-phase PCB + * + * February 2016, renamed as Mk2_3phase_RFdatalog_3 with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - The reported power at each of the phases has been inverted. These values are now in + * line with the Open Energy Monitor convention, whereby import is positive and + * export is negative. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include // may not be needed, but it's probably a good idea to include this + +// #define RF_PRESENT // <- this line should be commented out if the RFM12B module is not present + +#ifdef RF_PRESENT +#define RF69_COMPAT 0 // for the RFM12B +// #define RF69_COMPAT 1 // for the RF69 +#include +#endif + +// In this sketch, the ADC is free-running with a cycle time of ~104uS. + +// WORKLOAD_CHECK is available for determining how much spare processing time there +// is. To activate this mode, the #define line below should be included: +//#define WORKLOAD_CHECK + +#define CYCLES_PER_SECOND 50 +//#define JOULES_PER_WATT_HOUR 3600 // may be needed for datalogging +#define WORKING_ZONE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator +#define NO_OF_PHASES 3 +#define DATALOG_PERIOD_IN_SECONDS 5 + +const byte noOfDumploads = 3; + +enum polarities {NEGATIVE, POSITIVE}; +enum outputModes {ANTI_FLICKER, NORMAL}; +enum loadPriorityModes {LOAD_1_HAS_PRIORITY, LOAD_0_HAS_PRIORITY}; + +// enum loadStates {LOAD_ON, LOAD_OFF}; // for use if loads are active low (original PCB) +enum loadStates {LOAD_OFF, LOAD_ON}; // for use if loads are active high (Rev 2 PCB) +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +// For this multi-load version, the same mechanism has been retained but the +// output mode is hard-coded as below: +enum outputModes outputMode = ANTI_FLICKER; + +// In this multi-load version, the external switch is re-used to determine the load priority +enum loadPriorityModes loadPriorityMode = LOAD_0_HAS_PRIORITY; + +#ifdef RF_PRESENT +#define freq RF12_433MHZ // Use the freq to match the module you have. + +const int nodeID = 10; +const int networkGroup = 210; +const int UNO = 1; +#endif + +typedef struct { + int power_L1; + int power_L2; + int power_L3; + int Vrms_L1; + int Vrms_L2; + int Vrms_L3;} Tx_struct; // revised data for RF comms +Tx_struct tx_data; + + +// ----------- Pinout assignments ----------- +// +// digital pins: +const byte loadPrioritySelectorPin = 3; // // for 3-phase PCB +// D4 is not in use +const byte physicalLoad_0_pin = 5; // for 3-phase PCB, Load #1 (Rev 2 PCB) +const byte physicalLoad_1_pin = 6; // for 3-phase PCB, Load #2 (Rev 2 PCB) +const byte physicalLoad_2_pin = 7; // for 3-phase PCB, Load #3 (Rev 2 PCB) +// D8 is not in use +// D9 is not in use + +// analogue input pins +const byte sensorV[NO_OF_PHASES] = {0,2,4}; // for 3-phase PCB +const byte sensorI[NO_OF_PHASES] = {1,3,5}; // for 3-phase PCB + + +// -------------- general global variables ----------------- +// +// Some of these variables are used in multiple blocks so cannot be static. +// For integer maths, some variables need to be 'long' +// +boolean beyondStartUpPeriod = false; // start-up delay, allows things to settle +byte initialDelay = 3; // in seconds, to allow time to open the Serial monitor +byte startUpPeriod = 3; // in seconds, to allow LP filter to settle + +long DCoffset_V_long[NO_OF_PHASES]; // <--- for LPF +long DCoffset_V_min; // <--- for LPF (min limit) +long DCoffset_V_max; // <--- for LPF (max limit) +int DCoffset_I_nom; // nominal mid-point value of ADC @ x1 scale + +// for 3-phase use, with units of Joules * CYCLES_PER_SECOND +float capacityOfEnergyBucket_main; +float energyInBucket_main; +float midPointOfEnergyBucket_main; +float lowerThreshold_default; +float lowerEnergyThreshold; +float upperThreshold_default; +float upperEnergyThreshold; +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.4 + +// for improved control of multiple loads +boolean recentTransition = false; +byte postTransitionCount; +#define POST_TRANSITION_MAX_COUNT 3 // <-- allows each transition to take effect +//#define POST_TRANSITION_MAX_COUNT 50 // <-- for testing only +byte activeLoad = 0; + +// for datalogging +int datalogCountInMainsCycles; +const int maxDatalogCountInMainsCycles = DATALOG_PERIOD_IN_SECONDS * CYCLES_PER_SECOND; +float energyStateOfPhase[NO_OF_PHASES]; // only used for datalogging + +// for interaction between the main processor and the ISR +volatile boolean dataReady = false; +volatile int sampleV[NO_OF_PHASES]; +volatile int sampleI[NO_OF_PHASES]; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int mainsCycles_forContinuityChecker; +int lowestNoOfSampleSetsPerMainsCycle; + +// Calibration values +//------------------- +// Three calibration values are used in this sketch: powerCal, phaseCal and voltageCal. +// With most hardware, the default values are likely to work fine without +// need for change. A compact explanation of each of these values now follows: + +// When calculating real power, which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal[NO_OF_PHASES] = {0.043, 0.043, 0.043}; + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// NB. Any tool which determines the optimal value of phaseCal must have a similar +// scheme for taking sample values as does this sketch. +// +const float phaseCal[NO_OF_PHASES] = {0.5, 0.5, 0.5}; // <- nominal values only +int phaseCal_int[NO_OF_PHASES]; // to avoid the need for floating-point maths + +// For datalogging purposes, voltageCal has been added too. Because the range of ADC values is +// similar to the actual range of volts, the optimal value for this cal factor is likely to be +// close to unity. +const float voltageCal[NO_OF_PHASES] = {1.03, 1.03, 1.03}; // compared with Fluke 77 meter + + + + +void setup() +{ + delay (initialDelay * 1000); // allows time to open the Serial Monitor + + Serial.begin(9600); // initialize Serial interface + Serial.println(); + Serial.println(); + Serial.println(); + Serial.println("----------------------------------"); + Serial.println("Sketch ID: Mk2_3phase_RFdatalog_3.ino"); + + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for Load #1 + pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for Load #2 + pinMode(physicalLoad_2_pin, OUTPUT); // driver pin for Load #3 + + for(int i = 0; i< noOfDumploads; i++) + { + logicalLoadState[i] = LOAD_OFF; + } + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // update the local load's state. + digitalWrite(physicalLoad_1_pin, physicalLoadState[1]); // update the additional load state (inverse logic). + digitalWrite(physicalLoad_2_pin, physicalLoadState[2]); // update the additional load state (inverse logic). + + pinMode(loadPrioritySelectorPin, INPUT); + digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and + loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority mode + + for (byte phase = 0; phase < NO_OF_PHASES; phase++) + { + // When using integer maths, calibration values that have been supplied in + // floating point form need to be rescaled. + phaseCal_int[phase] = phaseCal[phase] * 256; // for integer maths + DCoffset_V_long[phase] = 512L * 256; // nominal mid-point value of ADC @ x256 scale + } + + // Define operating limits for the LP filters which identify DC offset in the voltage + // sample streams. By limiting the output range, these filters always should start up + // correctly. + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + DCoffset_I_nom = 512; // nominal mid-point value of ADC @ x1 scale + + // for the main energy bucket + capacityOfEnergyBucket_main = (float)WORKING_ZONE_IN_JOULES * CYCLES_PER_SECOND; + midPointOfEnergyBucket_main = capacityOfEnergyBucket_main * 0.5; // for resetting flexible thresholds + energyInBucket_main = 0; + + Serial.println ("ADC mode: free-running"); + Serial.print ("requiredExport in Watts = "); + Serial.println (REQUIRED_EXPORT_IN_WATTS); + + // Set up the ADC to be free-running + ADCSRA = (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples (3 pairs). The main processor and +// the ADC work autonomously, their operation being synchnonised only via the dataReady flag. +// +void processRawSamples() +{ + static long sumP[NO_OF_PHASES]; + static enum polarities polarityOfLastSampleV[NO_OF_PHASES]; // for zero-crossing detection + static long lastSampleV_minusDC_long[NO_OF_PHASES]; // for the phaseCal algorithm + static long cumVdeltasThisCycle_long[NO_OF_PHASES]; // for the LPF which determines DC offset (voltage) + static int samplesDuringThisMainsCycle[NO_OF_PHASES]; + static long sum_Vsquared[NO_OF_PHASES]; + static long samplesDuringThisDatalogPeriod; + enum polarities polarityNow; + + // The raw V and I samples are processed in "phase pairs" + for (byte phase = 0; phase < NO_OF_PHASES; phase++) + { + // remove DC offset from each raw voltage sample by subtracting the accurate value + // as determined by its associated LP filter. + long sampleV_minusDC_long = ((long)sampleV[phase]<<8) - DCoffset_V_long[phase]; + + // determine polarity, to aid the logical flow + if(sampleV_minusDC_long > 0) { + polarityNow = POSITIVE; } + else { + polarityNow = NEGATIVE; } + + if (polarityNow == POSITIVE) + { + if (polarityOfLastSampleV[phase] != POSITIVE) + { + if (beyondStartUpPeriod) + { + // This is the start of a new +ve half cycle, for this phase, just after the + // zero-crossing point. Before the contribution from this phase can be added + // to the running total, the cal factor for this phase must be applied. + // + float realPower = (sumP[phase] / samplesDuringThisMainsCycle[phase]) * powerCal[phase]; + + processLatestContribution(phase, realPower); // runs at 6.6 ms intervals + + // A performance check to monitor and display the minimum number of sets of + // ADC samples per mains cycle, the expected number being 20ms / (104us * 6) = 32.05 + // + if (phase == 0) + { + if (samplesDuringThisMainsCycle[phase] < lowestNoOfSampleSetsPerMainsCycle) + { + lowestNoOfSampleSetsPerMainsCycle = samplesDuringThisMainsCycle[phase]; + } + mainsCycles_forContinuityChecker++; + if (mainsCycles_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + mainsCycles_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + } + + sumP[phase] = 0; + samplesDuringThisMainsCycle[phase] = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (initialDelay + startUpPeriod) * 1000) + { + beyondStartUpPeriod = true; + mainsCycles_forContinuityChecker = 1; // opportunity has been missed for this cycle + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + // check to see whether the trigger device can now be reliably armed + if ((phase == 0) && samplesDuringThisMainsCycle[0] == 2) // lower value for larger sample set + { + if (beyondStartUpPeriod) + { + // This code is executed once per 20mS, shortly after the start of each new + // mains cycle on phase 0. + // + datalogCountInMainsCycles++; + + // Changling the state of the loads is is a 3-part process: + // - change the LOGICAL load states as necessary to maintain the energy level + // - update the PHYSICAL load states according to the logical -> physical mapping + // - update the driver lines for each of the loads. + // + // Restrictions apply for the period immediately after a load has been switched. + // Here the recentTransition flag is checked and updated as necessary. + if (recentTransition) + { + postTransitionCount++; + if (postTransitionCount >= POST_TRANSITION_MAX_COUNT) + { + recentTransition = false; + } + } + + if (energyInBucket_main > midPointOfEnergyBucket_main) + { + // the energy state is in the upper half of the working range + lowerEnergyThreshold = lowerThreshold_default; // reset the "opposite" threshold + if (energyInBucket_main > upperEnergyThreshold) + { + // Because the energy level is high, some action may be required + boolean OK_toAddLoad = true; + byte tempLoad = nextLogicalLoadToBeAdded(); + if (tempLoad < noOfDumploads) + { + // a load which is now OFF has been identified for potentially being switched ON + if (recentTransition) + { + // During the post-transition period, any increase in the energy level is noted. + if (energyInBucket_main > upperEnergyThreshold) + { + upperEnergyThreshold = energyInBucket_main; + + // the energy thresholds must remain within range + if (upperEnergyThreshold > capacityOfEnergyBucket_main) + { + upperEnergyThreshold = capacityOfEnergyBucket_main; + } + } + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toAddLoad = false; + } + } + + if (OK_toAddLoad) + { + logicalLoadState[tempLoad] = LOAD_ON; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + Serial.print('+'); + Serial.println(activeLoad); + } + } + } + } + else + { // the energy state is in the lower half of the working range + upperEnergyThreshold = upperThreshold_default; // reset the "opposite" threshold + if (energyInBucket_main < lowerEnergyThreshold) + { + // Because the energy level is low, some action may be required + boolean OK_toRemoveLoad = true; + byte tempLoad = nextLogicalLoadToBeRemoved(); + if (tempLoad < noOfDumploads) + { + // a load which is now ON has been identified for potentially being switched OFF + if (recentTransition) + { + // During the post-transition period, any decrease in the energy level is noted. + if (energyInBucket_main < lowerEnergyThreshold) + { + lowerEnergyThreshold = energyInBucket_main; + + // the energy thresholds must remain within range + if (lowerEnergyThreshold < 0) + { + lowerEnergyThreshold = 0; + } + } + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toRemoveLoad = false; + } + } + + if (OK_toRemoveLoad) + { + logicalLoadState[tempLoad] = LOAD_OFF; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + Serial.print('-'); + Serial.println(activeLoad); + } + } + } + } + + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + // update the control ports for each of the physical loads + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger + digitalWrite(physicalLoad_1_pin, physicalLoadState[1]); // active low for trigger + digitalWrite(physicalLoad_2_pin, physicalLoadState[2]); // active low for trigger + + // Now that the energy-related decisions have been taken, min and max limits can now + // be applied to the level of the energy bucket. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_main > capacityOfEnergyBucket_main) { + energyInBucket_main = capacityOfEnergyBucket_main; } + else + if (energyInBucket_main < 0) { + energyInBucket_main = 0; } + + if (datalogCountInMainsCycles >= maxDatalogCountInMainsCycles) + { + datalogCountInMainsCycles = 0; + + tx_data.power_L1 = energyStateOfPhase[0] / maxDatalogCountInMainsCycles; + tx_data.power_L1 *= -1; // to match the OEM convention (import is =ve; export is -ve) + tx_data.power_L2 = energyStateOfPhase[1] / maxDatalogCountInMainsCycles; + tx_data.power_L2 *= -1; // to match the OEM convention (import is =ve; export is -ve) + tx_data.power_L3 = energyStateOfPhase[2] / maxDatalogCountInMainsCycles; + tx_data.power_L3 *= -1; // to match the OEM convention (import is =ve; export is -ve) + tx_data.Vrms_L1 = (int)(voltageCal[0] * sqrt(sum_Vsquared[0] / samplesDuringThisDatalogPeriod)); + tx_data.Vrms_L2 = (int)(voltageCal[1] * sqrt(sum_Vsquared[1] / samplesDuringThisDatalogPeriod)); + tx_data.Vrms_L3 = (int)(voltageCal[2] * sqrt(sum_Vsquared[2] / samplesDuringThisDatalogPeriod)); +#ifdef RF_PRESENT + send_rf_data(); +#endif +/* <-- Warning - Unlike its 1-phase equivalent, this 3-phase code can be affected by Serial statements! + Serial.print(energyInBucket_main / CYCLES_PER_SECOND); + Serial.print(", "); +// + Serial.print(tx_data.power_L1); + Serial.print(", "); + Serial.print(tx_data.power_L2); + Serial.print(", "); + Serial.print(tx_data.power_L3); + Serial.print(", "); + Serial.print(tx_data.Vrms_L1); + Serial.print(", "); + Serial.print(tx_data.Vrms_L2); + Serial.print(", "); + Serial.println(tx_data.Vrms_L3); +*/ + energyStateOfPhase[0] = 0; + energyStateOfPhase[1] = 0; + energyStateOfPhase[2] = 0; + sum_Vsquared[0] = 0; + sum_Vsquared[1] = 0; + sum_Vsquared[2] = 0; + samplesDuringThisDatalogPeriod = 0; + + +/* <-- Warning - Unlike its 1-phase equivalent, this 3-phase code can be affected by Serial statements! + for (int i = 0; i < noOfDumploads; i++) + { + Serial.print(logicalLoadState[i]); + } + Serial.println(); +*/ + } + } + } + } // end of processing that is specific to samples where the voltage is positive + + else // the polarity of this sample is negative + { + if (polarityOfLastSampleV[phase] != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // This is a convenient point to update the Low Pass Filter for removing the DC + // component from the phase that is being processed. + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + DCoffset_V_long[phase] += (cumVdeltasThisCycle_long[phase]>>12); + cumVdeltasThisCycle_long[phase] = 0; + + // To ensure that this LP filter will always start up correctly when 240V AC is + // available, its output value needs to be prevented from drifting beyond the likely range + // of the voltage signal. + // + if (DCoffset_V_long[phase] < DCoffset_V_min) { + DCoffset_V_long[phase] = DCoffset_V_min; } + else + if (DCoffset_V_long[phase] > DCoffset_V_max) { + DCoffset_V_long[phase] = DCoffset_V_max; } + + if (phase == 0) + { + checkLoadPrioritySelection(); // updates load priorities if the switch is changed + } + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is negative + + // Processing for EVERY pair of samples. Most of this code is not used during the + // start-up period, but it does no harm to leave it in place. Accumulated values + // are cleared when the beyondStartUpPhase flag is set to true. + // + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_long = ((long)(sampleI[phase] - DCoffset_I_nom))<<8; + + // phase-shift the voltage waveform so that it aligns with the current when a + // resistive load is used + long phaseShiftedSampleV_minusDC_long = lastSampleV_minusDC_long[phase] + + (((sampleV_minusDC_long - lastSampleV_minusDC_long[phase])*phaseCal_int[phase])>>8); + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleV_minusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP[phase] +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // for the Vrms calculation (for datalogging only) + long inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (now x4096, or 2^12) + inst_Vsquared = inst_Vsquared>>12; // scaling is now x1 (V_ADC x I_ADC) + sum_Vsquared[phase] += inst_Vsquared; // cumulative V^2 (V_ADC x I_ADC) + if (phase == 0) { + samplesDuringThisDatalogPeriod ++; } // no need to keep separate counts for each phase + + // general housekeeping + cumVdeltasThisCycle_long[phase] += sampleV_minusDC_long; // for use with LP filter + samplesDuringThisMainsCycle[phase] ++; + + // store items for use during next loop + lastSampleV_minusDC_long[phase] = sampleV_minusDC_long; // required for phaseCal algorithm + polarityOfLastSampleV[phase] = polarityNow; // for identification of half cycle boundaries + } +} +// end of processRawSamples() + + +void processLatestContribution(byte phase, float power) +{ + float latestEnergyContribution = power; // for efficiency, the energy scale is Joules * CYCLES_PER_SECOND + + // add the latest energy contribution to the relevant per-phase accumulator + // (only used for datalogging of power) + energyStateOfPhase[phase] += latestEnergyContribution; + + // add the latest energy contribution to the main energy accumulator + energyInBucket_main += latestEnergyContribution; + + // apply any adjustment that is required. + if (phase == 0) + { + energyInBucket_main -= REQUIRED_EXPORT_IN_WATTS; // energy scale is Joules x 50 + } + + // Applying max and min limits to the main accumulator's level + // is deferred until after the energy related decisions have been taken + // +} + +byte nextLogicalLoadToBeAdded() +{ + byte retVal = noOfDumploads; + boolean success = false; + + for (byte index = 0; index < noOfDumploads && !success; index++) + { + if (logicalLoadState[index] == LOAD_OFF) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +byte nextLogicalLoadToBeRemoved() +{ + byte retVal = noOfDumploads; + boolean success = false; + + // NB. the index cannot be a 'byte' because the loop would not terminate correctly! + for (char index = (noOfDumploads -1); index >= 0 && !success; index--) + { + if (logicalLoadState[index] == LOAD_ON) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * The association between the physical and logical loads is 1:1. By default, numerical + * equivalence is maintained, so logical(N) maps to physical(N). If physical load 1 is set + * to have priority, rather than physical load 0, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * Any other mapping relaionships could be configured here. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } + + if (loadPriorityMode == LOAD_1_HAS_PRIORITY) + { + // swap physical loads 0 & 1 if remote load has priority + physicalLoadState[0] = logicalLoadState[1]; + physicalLoadState[1] = logicalLoadState[0]; + } +} + + +// this function changes the value of the load priorities if the state of the external switch is altered +void checkLoadPrioritySelection() +{ + static byte loadPrioritySwitchCcount = 0; + int pinState = digitalRead(loadPrioritySelectorPin); + if (pinState != loadPriorityMode) + { + loadPrioritySwitchCcount++; + } + if (loadPrioritySwitchCcount >= 20) + { + loadPrioritySwitchCcount = 0; + loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable + Serial.print ("loadPriority selection changed to "); + if (loadPriorityMode == LOAD_0_HAS_PRIORITY) { + Serial.println ( "load 0"); } + else { + Serial.println ( "load 1"); } + } +} + + +// Although this sketch always operates in ANTI_FLICKER mode, it was convenient +// to leave this mechanism in place. +// +void configureParamsForSelectedOutputMode() +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerThreshold_default = + capacityOfEnergyBucket_main * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperThreshold_default = + capacityOfEnergyBucket_main * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerThreshold_default = capacityOfEnergyBucket_main * 0.5; + upperThreshold_default = capacityOfEnergyBucket_main * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_main = "); + Serial.println(capacityOfEnergyBucket_main); + Serial.print(" lowerEnergyThreshold = "); + Serial.println(lowerThreshold_default); + Serial.print(" upperEnergyThreshold = "); + Serial.println(upperThreshold_default); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + + +#ifdef RF_PRESENT +void send_rf_data() +{ + int i = 0; + while (!rf12_canSend() && i<10) + { + rf12_recvDone(); + i++; + } + rf12_sendStart(0, &tx_data, sizeof tx_data); +} +#endif + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + + + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_3a.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_3a.ino new file mode 100644 index 0000000..29a7b98 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_3a.ino @@ -0,0 +1,964 @@ +/* Mk2_3phase_RFdatalog_3a.ino + * + * Issue 1 was released in January 2015. + * + * This sketch provides continuous monitoring of real power on three phases. + * Surplus power is diverted to multiple loads in sequential order. A suitable + * output-stage is required for each load; this can be either triac-based, or a + * Solid State Relay. + * + * Datalogging of real power and Vrms is provided for each phase. + * The presence or absence of the RFM12B needs to be set at compile time + * + * January 2016, renamed as Mk2_3phase_RFdatalog_2 with these changes: + * - Improved control of multiple loads has been imported from the + * equivalent 1-phase sketch, Mk2_multiLoad_wired_6.ino + * - the ISR has been upgraded to fix a possible timing anomaly + * - variables to store ADC samples are now declared as "volatile" + * - for RF69 RF module is now supported + * - a performance check has been added with the result being sent to the Serial port + * - control signals for loads are now active-high to suit the latest 3-phase PCB + * + * February 2016, renamed as Mk2_3phase_RFdatalog_3 with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - The reported power at each of the phases has been inverted. These values are now in + * line with the Open Energy Monitor convention, whereby import is positive and + * export is negative. + * + * February 2020: updated to Mk2_3phase_RFdatalog_3a with these changes: + * - removal of some redundant code in the logic for determining the next load state. + * + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include // may not be needed, but it's probably a good idea to include this + +// #define RF_PRESENT // <- this line should be commented out if the RFM12B module is not present + +#ifdef RF_PRESENT +#define RF69_COMPAT 0 // for the RFM12B +// #define RF69_COMPAT 1 // for the RF69 +#include +#endif + +// In this sketch, the ADC is free-running with a cycle time of ~104uS. + +// WORKLOAD_CHECK is available for determining how much spare processing time there +// is. To activate this mode, the #define line below should be included: +//#define WORKLOAD_CHECK + +#define CYCLES_PER_SECOND 50 +//#define JOULES_PER_WATT_HOUR 3600 // may be needed for datalogging +#define WORKING_ZONE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator +#define NO_OF_PHASES 3 +#define DATALOG_PERIOD_IN_SECONDS 5 + +const byte noOfDumploads = 3; + +enum polarities {NEGATIVE, POSITIVE}; +enum outputModes {ANTI_FLICKER, NORMAL}; +enum loadPriorityModes {LOAD_1_HAS_PRIORITY, LOAD_0_HAS_PRIORITY}; + +// enum loadStates {LOAD_ON, LOAD_OFF}; // for use if loads are active low (original PCB) +enum loadStates {LOAD_OFF, LOAD_ON}; // for use if loads are active high (Rev 2 PCB) +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +// For this multi-load version, the same mechanism has been retained but the +// output mode is hard-coded as below: +enum outputModes outputMode = ANTI_FLICKER; + +// In this multi-load version, the external switch is re-used to determine the load priority +enum loadPriorityModes loadPriorityMode = LOAD_0_HAS_PRIORITY; + +#ifdef RF_PRESENT +#define freq RF12_433MHZ // Use the freq to match the module you have. + +const int nodeID = 10; +const int networkGroup = 210; +const int UNO = 1; +#endif + +typedef struct { + int power_L1; + int power_L2; + int power_L3; + int Vrms_L1; + int Vrms_L2; + int Vrms_L3;} Tx_struct; // revised data for RF comms +Tx_struct tx_data; + + +// ----------- Pinout assignments ----------- +// +// digital pins: +const byte loadPrioritySelectorPin = 3; // // for 3-phase PCB +// D4 is not in use +const byte physicalLoad_0_pin = 5; // for 3-phase PCB, Load #1 (Rev 2 PCB) +const byte physicalLoad_1_pin = 6; // for 3-phase PCB, Load #2 (Rev 2 PCB) +const byte physicalLoad_2_pin = 7; // for 3-phase PCB, Load #3 (Rev 2 PCB) +// D8 is not in use +// D9 is not in use + +// analogue input pins +const byte sensorV[NO_OF_PHASES] = {0,2,4}; // for 3-phase PCB +const byte sensorI[NO_OF_PHASES] = {1,3,5}; // for 3-phase PCB + + +// -------------- general global variables ----------------- +// +// Some of these variables are used in multiple blocks so cannot be static. +// For integer maths, some variables need to be 'long' +// +boolean beyondStartUpPeriod = false; // start-up delay, allows things to settle +byte initialDelay = 3; // in seconds, to allow time to open the Serial monitor +byte startUpPeriod = 3; // in seconds, to allow LP filter to settle + +long DCoffset_V_long[NO_OF_PHASES]; // <--- for LPF +long DCoffset_V_min; // <--- for LPF (min limit) +long DCoffset_V_max; // <--- for LPF (max limit) +int DCoffset_I_nom; // nominal mid-point value of ADC @ x1 scale + +// for 3-phase use, with units of Joules * CYCLES_PER_SECOND +float capacityOfEnergyBucket_main; +float energyInBucket_main; +float midPointOfEnergyBucket_main; +float lowerThreshold_default; +float lowerEnergyThreshold; +float upperThreshold_default; +float upperEnergyThreshold; +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.4 + +// for improved control of multiple loads +boolean recentTransition = false; +byte postTransitionCount; +#define POST_TRANSITION_MAX_COUNT 3 // <-- allows each transition to take effect +//#define POST_TRANSITION_MAX_COUNT 50 // <-- for testing only +byte activeLoad = 0; + +// for datalogging +int datalogCountInMainsCycles; +const int maxDatalogCountInMainsCycles = DATALOG_PERIOD_IN_SECONDS * CYCLES_PER_SECOND; +float energyStateOfPhase[NO_OF_PHASES]; // only used for datalogging + +// for interaction between the main processor and the ISR +volatile boolean dataReady = false; +volatile int sampleV[NO_OF_PHASES]; +volatile int sampleI[NO_OF_PHASES]; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int mainsCycles_forContinuityChecker; +int lowestNoOfSampleSetsPerMainsCycle; + +// Calibration values +//------------------- +// Three calibration values are used in this sketch: powerCal, phaseCal and voltageCal. +// With most hardware, the default values are likely to work fine without +// need for change. A compact explanation of each of these values now follows: + +// When calculating real power, which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal[NO_OF_PHASES] = {0.043, 0.043, 0.043}; + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// NB. Any tool which determines the optimal value of phaseCal must have a similar +// scheme for taking sample values as does this sketch. +// +const float phaseCal[NO_OF_PHASES] = {0.5, 0.5, 0.5}; // <- nominal values only +int phaseCal_int[NO_OF_PHASES]; // to avoid the need for floating-point maths + +// For datalogging purposes, voltageCal has been added too. Because the range of ADC values is +// similar to the actual range of volts, the optimal value for this cal factor is likely to be +// close to unity. +const float voltageCal[NO_OF_PHASES] = {1.03, 1.03, 1.03}; // compared with Fluke 77 meter + + + + +void setup() +{ + delay (initialDelay * 1000); // allows time to open the Serial Monitor + + Serial.begin(9600); // initialize Serial interface + Serial.println(); + Serial.println(); + Serial.println(); + Serial.println("----------------------------------"); + Serial.println("Sketch ID: Mk2_3phase_RFdatalog_3a.ino"); + + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for Load #1 + pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for Load #2 + pinMode(physicalLoad_2_pin, OUTPUT); // driver pin for Load #3 + + for(int i = 0; i< noOfDumploads; i++) + { + logicalLoadState[i] = LOAD_OFF; + } + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // update the local load's state. + digitalWrite(physicalLoad_1_pin, physicalLoadState[1]); // update the additional load state (inverse logic). + digitalWrite(physicalLoad_2_pin, physicalLoadState[2]); // update the additional load state (inverse logic). + + pinMode(loadPrioritySelectorPin, INPUT); + digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and + loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority mode + + for (byte phase = 0; phase < NO_OF_PHASES; phase++) + { + // When using integer maths, calibration values that have been supplied in + // floating point form need to be rescaled. + phaseCal_int[phase] = phaseCal[phase] * 256; // for integer maths + DCoffset_V_long[phase] = 512L * 256; // nominal mid-point value of ADC @ x256 scale + } + + // Define operating limits for the LP filters which identify DC offset in the voltage + // sample streams. By limiting the output range, these filters always should start up + // correctly. + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + DCoffset_I_nom = 512; // nominal mid-point value of ADC @ x1 scale + + // for the main energy bucket + capacityOfEnergyBucket_main = (float)WORKING_ZONE_IN_JOULES * CYCLES_PER_SECOND; + midPointOfEnergyBucket_main = capacityOfEnergyBucket_main * 0.5; // for resetting flexible thresholds + energyInBucket_main = 0; + + Serial.println ("ADC mode: free-running"); + Serial.print ("requiredExport in Watts = "); + Serial.println (REQUIRED_EXPORT_IN_WATTS); + + // Set up the ADC to be free-running + ADCSRA = (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples (3 pairs). The main processor and +// the ADC work autonomously, their operation being synchnonised only via the dataReady flag. +// +void processRawSamples() +{ + static long sumP[NO_OF_PHASES]; + static enum polarities polarityOfLastSampleV[NO_OF_PHASES]; // for zero-crossing detection + static long lastSampleV_minusDC_long[NO_OF_PHASES]; // for the phaseCal algorithm + static long cumVdeltasThisCycle_long[NO_OF_PHASES]; // for the LPF which determines DC offset (voltage) + static int samplesDuringThisMainsCycle[NO_OF_PHASES]; + static long sum_Vsquared[NO_OF_PHASES]; + static long samplesDuringThisDatalogPeriod; + enum polarities polarityNow; + + // The raw V and I samples are processed in "phase pairs" + for (byte phase = 0; phase < NO_OF_PHASES; phase++) + { + // remove DC offset from each raw voltage sample by subtracting the accurate value + // as determined by its associated LP filter. + long sampleV_minusDC_long = ((long)sampleV[phase]<<8) - DCoffset_V_long[phase]; + + // determine polarity, to aid the logical flow + if(sampleV_minusDC_long > 0) { + polarityNow = POSITIVE; } + else { + polarityNow = NEGATIVE; } + + if (polarityNow == POSITIVE) + { + if (polarityOfLastSampleV[phase] != POSITIVE) + { + if (beyondStartUpPeriod) + { + // This is the start of a new +ve half cycle, for this phase, just after the + // zero-crossing point. Before the contribution from this phase can be added + // to the running total, the cal factor for this phase must be applied. + // + float realPower = (sumP[phase] / samplesDuringThisMainsCycle[phase]) * powerCal[phase]; + + processLatestContribution(phase, realPower); // runs at 6.6 ms intervals + + // A performance check to monitor and display the minimum number of sets of + // ADC samples per mains cycle, the expected number being 20ms / (104us * 6) = 32.05 + // + if (phase == 0) + { + if (samplesDuringThisMainsCycle[phase] < lowestNoOfSampleSetsPerMainsCycle) + { + lowestNoOfSampleSetsPerMainsCycle = samplesDuringThisMainsCycle[phase]; + } + mainsCycles_forContinuityChecker++; + if (mainsCycles_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + mainsCycles_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + } + + sumP[phase] = 0; + samplesDuringThisMainsCycle[phase] = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (initialDelay + startUpPeriod) * 1000) + { + beyondStartUpPeriod = true; + mainsCycles_forContinuityChecker = 1; // opportunity has been missed for this cycle + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + // check to see whether the trigger device can now be reliably armed + if ((phase == 0) && samplesDuringThisMainsCycle[0] == 2) // lower value for larger sample set + { + if (beyondStartUpPeriod) + { + // This code is executed once per 20mS, shortly after the start of each new + // mains cycle on phase 0. + // + datalogCountInMainsCycles++; + + // Changling the state of the loads is is a 3-part process: + // - change the LOGICAL load states as necessary to maintain the energy level + // - update the PHYSICAL load states according to the logical -> physical mapping + // - update the driver lines for each of the loads. + // + // Restrictions apply for the period immediately after a load has been switched. + // Here the recentTransition flag is checked and updated as necessary. + if (recentTransition) + { + postTransitionCount++; + if (postTransitionCount >= POST_TRANSITION_MAX_COUNT) + { + recentTransition = false; + } + } + + if (energyInBucket_main > midPointOfEnergyBucket_main) + { + // the energy state is in the upper half of the working range + lowerEnergyThreshold = lowerThreshold_default; // reset the "opposite" threshold + if (energyInBucket_main > upperEnergyThreshold) + { + // Because the energy level is high, some action may be required + boolean OK_toAddLoad = true; + byte tempLoad = nextLogicalLoadToBeAdded(); + if (tempLoad < noOfDumploads) + { + // a load which is now OFF has been identified for potentially being switched ON + if (recentTransition) + { + // During the post-transition period, any increase in the energy level is noted. + upperEnergyThreshold = energyInBucket_main; + + // the energy thresholds must remain within range + if (upperEnergyThreshold > capacityOfEnergyBucket_main) + { + upperEnergyThreshold = capacityOfEnergyBucket_main; + } + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toAddLoad = false; + } + } + + if (OK_toAddLoad) + { + logicalLoadState[tempLoad] = LOAD_ON; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + Serial.print('+'); + Serial.println(activeLoad); + } + } + } + } + else + { // the energy state is in the lower half of the working range + upperEnergyThreshold = upperThreshold_default; // reset the "opposite" threshold + if (energyInBucket_main < lowerEnergyThreshold) + { + // Because the energy level is low, some action may be required + boolean OK_toRemoveLoad = true; + byte tempLoad = nextLogicalLoadToBeRemoved(); + if (tempLoad < noOfDumploads) + { + // a load which is now ON has been identified for potentially being switched OFF + if (recentTransition) + { + // During the post-transition period, any decrease in the energy level is noted. + lowerEnergyThreshold = energyInBucket_main; + + // the energy thresholds must remain within range + if (lowerEnergyThreshold < 0) + { + lowerEnergyThreshold = 0; + } + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toRemoveLoad = false; + } + } + + if (OK_toRemoveLoad) + { + logicalLoadState[tempLoad] = LOAD_OFF; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + Serial.print('-'); + Serial.println(activeLoad); + } + } + } + } + + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + // update the control ports for each of the physical loads + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger + digitalWrite(physicalLoad_1_pin, physicalLoadState[1]); // active low for trigger + digitalWrite(physicalLoad_2_pin, physicalLoadState[2]); // active low for trigger + + // Now that the energy-related decisions have been taken, min and max limits can now + // be applied to the level of the energy bucket. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_main > capacityOfEnergyBucket_main) { + energyInBucket_main = capacityOfEnergyBucket_main; } + else + if (energyInBucket_main < 0) { + energyInBucket_main = 0; } + + if (datalogCountInMainsCycles >= maxDatalogCountInMainsCycles) + { + datalogCountInMainsCycles = 0; + + tx_data.power_L1 = energyStateOfPhase[0] / maxDatalogCountInMainsCycles; + tx_data.power_L1 *= -1; // to match the OEM convention (import is =ve; export is -ve) + tx_data.power_L2 = energyStateOfPhase[1] / maxDatalogCountInMainsCycles; + tx_data.power_L2 *= -1; // to match the OEM convention (import is =ve; export is -ve) + tx_data.power_L3 = energyStateOfPhase[2] / maxDatalogCountInMainsCycles; + tx_data.power_L3 *= -1; // to match the OEM convention (import is =ve; export is -ve) + tx_data.Vrms_L1 = (int)(voltageCal[0] * sqrt(sum_Vsquared[0] / samplesDuringThisDatalogPeriod)); + tx_data.Vrms_L2 = (int)(voltageCal[1] * sqrt(sum_Vsquared[1] / samplesDuringThisDatalogPeriod)); + tx_data.Vrms_L3 = (int)(voltageCal[2] * sqrt(sum_Vsquared[2] / samplesDuringThisDatalogPeriod)); +#ifdef RF_PRESENT + send_rf_data(); +#endif +/* <-- Warning - Unlike its 1-phase equivalent, this 3-phase code can be affected by Serial statements! + Serial.print(energyInBucket_main / CYCLES_PER_SECOND); + Serial.print(", "); +// + Serial.print(tx_data.power_L1); + Serial.print(", "); + Serial.print(tx_data.power_L2); + Serial.print(", "); + Serial.print(tx_data.power_L3); + Serial.print(", "); + Serial.print(tx_data.Vrms_L1); + Serial.print(", "); + Serial.print(tx_data.Vrms_L2); + Serial.print(", "); + Serial.println(tx_data.Vrms_L3); +*/ + energyStateOfPhase[0] = 0; + energyStateOfPhase[1] = 0; + energyStateOfPhase[2] = 0; + sum_Vsquared[0] = 0; + sum_Vsquared[1] = 0; + sum_Vsquared[2] = 0; + samplesDuringThisDatalogPeriod = 0; + + +/* <-- Warning - Unlike its 1-phase equivalent, this 3-phase code can be affected by Serial statements! + for (int i = 0; i < noOfDumploads; i++) + { + Serial.print(logicalLoadState[i]); + } + Serial.println(); +*/ + } + } + } + } // end of processing that is specific to samples where the voltage is positive + + else // the polarity of this sample is negative + { + if (polarityOfLastSampleV[phase] != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // This is a convenient point to update the Low Pass Filter for removing the DC + // component from the phase that is being processed. + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + DCoffset_V_long[phase] += (cumVdeltasThisCycle_long[phase]>>12); + cumVdeltasThisCycle_long[phase] = 0; + + // To ensure that this LP filter will always start up correctly when 240V AC is + // available, its output value needs to be prevented from drifting beyond the likely range + // of the voltage signal. + // + if (DCoffset_V_long[phase] < DCoffset_V_min) { + DCoffset_V_long[phase] = DCoffset_V_min; } + else + if (DCoffset_V_long[phase] > DCoffset_V_max) { + DCoffset_V_long[phase] = DCoffset_V_max; } + + if (phase == 0) + { + checkLoadPrioritySelection(); // updates load priorities if the switch is changed + } + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is negative + + // Processing for EVERY pair of samples. Most of this code is not used during the + // start-up period, but it does no harm to leave it in place. Accumulated values + // are cleared when the beyondStartUpPhase flag is set to true. + // + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_long = ((long)(sampleI[phase] - DCoffset_I_nom))<<8; + + // phase-shift the voltage waveform so that it aligns with the current when a + // resistive load is used + long phaseShiftedSampleV_minusDC_long = lastSampleV_minusDC_long[phase] + + (((sampleV_minusDC_long - lastSampleV_minusDC_long[phase])*phaseCal_int[phase])>>8); + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleV_minusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP[phase] +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // for the Vrms calculation (for datalogging only) + long inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (now x4096, or 2^12) + inst_Vsquared = inst_Vsquared>>12; // scaling is now x1 (V_ADC x I_ADC) + sum_Vsquared[phase] += inst_Vsquared; // cumulative V^2 (V_ADC x I_ADC) + if (phase == 0) { + samplesDuringThisDatalogPeriod ++; } // no need to keep separate counts for each phase + + // general housekeeping + cumVdeltasThisCycle_long[phase] += sampleV_minusDC_long; // for use with LP filter + samplesDuringThisMainsCycle[phase] ++; + + // store items for use during next loop + lastSampleV_minusDC_long[phase] = sampleV_minusDC_long; // required for phaseCal algorithm + polarityOfLastSampleV[phase] = polarityNow; // for identification of half cycle boundaries + } +} +// end of processRawSamples() + + +void processLatestContribution(byte phase, float power) +{ + float latestEnergyContribution = power; // for efficiency, the energy scale is Joules * CYCLES_PER_SECOND + + // add the latest energy contribution to the relevant per-phase accumulator + // (only used for datalogging of power) + energyStateOfPhase[phase] += latestEnergyContribution; + + // add the latest energy contribution to the main energy accumulator + energyInBucket_main += latestEnergyContribution; + + // apply any adjustment that is required. + if (phase == 0) + { + energyInBucket_main -= REQUIRED_EXPORT_IN_WATTS; // energy scale is Joules x 50 + } + + // Applying max and min limits to the main accumulator's level + // is deferred until after the energy related decisions have been taken + // +} + +byte nextLogicalLoadToBeAdded() +{ + byte retVal = noOfDumploads; + boolean success = false; + + for (byte index = 0; index < noOfDumploads && !success; index++) + { + if (logicalLoadState[index] == LOAD_OFF) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +byte nextLogicalLoadToBeRemoved() +{ + byte retVal = noOfDumploads; + boolean success = false; + + // NB. the index cannot be a 'byte' because the loop would not terminate correctly! + for (char index = (noOfDumploads -1); index >= 0 && !success; index--) + { + if (logicalLoadState[index] == LOAD_ON) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * The association between the physical and logical loads is 1:1. By default, numerical + * equivalence is maintained, so logical(N) maps to physical(N). If physical load 1 is set + * to have priority, rather than physical load 0, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * Any other mapping relaionships could be configured here. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } + + if (loadPriorityMode == LOAD_1_HAS_PRIORITY) + { + // swap physical loads 0 & 1 if remote load has priority + physicalLoadState[0] = logicalLoadState[1]; + physicalLoadState[1] = logicalLoadState[0]; + } +} + + +// this function changes the value of the load priorities if the state of the external switch is altered +void checkLoadPrioritySelection() +{ + static byte loadPrioritySwitchCcount = 0; + int pinState = digitalRead(loadPrioritySelectorPin); + if (pinState != loadPriorityMode) + { + loadPrioritySwitchCcount++; + } + if (loadPrioritySwitchCcount >= 20) + { + loadPrioritySwitchCcount = 0; + loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable + Serial.print ("loadPriority selection changed to "); + if (loadPriorityMode == LOAD_0_HAS_PRIORITY) { + Serial.println ( "load 0"); } + else { + Serial.println ( "load 1"); } + } +} + + +// Although this sketch always operates in ANTI_FLICKER mode, it was convenient +// to leave this mechanism in place. +// +void configureParamsForSelectedOutputMode() +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerThreshold_default = + capacityOfEnergyBucket_main * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperThreshold_default = + capacityOfEnergyBucket_main * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerThreshold_default = capacityOfEnergyBucket_main * 0.5; + upperThreshold_default = capacityOfEnergyBucket_main * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_main = "); + Serial.println(capacityOfEnergyBucket_main); + Serial.print(" lowerEnergyThreshold = "); + Serial.println(lowerThreshold_default); + Serial.print(" upperEnergyThreshold = "); + Serial.println(upperThreshold_default); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + + +#ifdef RF_PRESENT +void send_rf_data() +{ + int i = 0; + while (!rf12_canSend() && i<10) + { + rf12_recvDone(); + i++; + } + rf12_sendStart(0, &tx_data, sizeof tx_data); +} +#endif + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + + + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_4.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_4.ino new file mode 100644 index 0000000..acc1ca1 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_3phase_RFdatalog_4.ino @@ -0,0 +1,971 @@ +/* Mk2_3phase_RFdatalog_4.ino + * + * Issue 1 was released in January 2015. + * + * This sketch provides continuous monitoring of real power on three phases. + * Surplus power is diverted to multiple loads in sequential order. A suitable + * output-stage is required for each load; this can be either triac-based, or a + * Solid State Relay. + * + * Datalogging of real power and Vrms is provided for each phase. + * The presence or absence of the RFM12B needs to be set at compile time + * + * January 2016, renamed as Mk2_3phase_RFdatalog_2 with these changes: + * - Improved control of multiple loads has been imported from the + * equivalent 1-phase sketch, Mk2_multiLoad_wired_6.ino + * - the ISR has been upgraded to fix a possible timing anomaly + * - variables to store ADC samples are now declared as "volatile" + * - for RF69 RF module is now supported + * - a performance check has been added with the result being sent to the Serial port + * - control signals for loads are now active-high to suit the latest 3-phase PCB + * + * February 2016, renamed as Mk2_3phase_RFdatalog_3 with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - The reported power at each of the phases has been inverted. These values are now in + * line with the Open Energy Monitor convention, whereby import is positive and + * export is negative. + * + * February 2020: updated to Mk2_3phase_RFdatalog_3a with these changes: + * - removal of some redundant code in the logic for determining the next load state. + * + * July 2022: updated to Mk2_3phase_RFdatalog_4, with this change: + * - the datalogging accumulator for Vsquared has been rescaled to 1/16 of its previous value + * to avoid the risk of overflowing during a 20-second datalogging period. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include // may not be needed, but it's probably a good idea to include this + +#define RF_PRESENT // <- this line should be commented out if the RFM12B module is not present + +#ifdef RF_PRESENT +//#define RF69_COMPAT 0 // for the RFM12B +#define RF69_COMPAT 1 // for the RF69 +#include +#endif + +// In this sketch, the ADC is free-running with a cycle time of ~104uS. + +// WORKLOAD_CHECK is available for determining how much spare processing time there +// is. To activate this mode, the #define line below should be included: +//#define WORKLOAD_CHECK + +#define CYCLES_PER_SECOND 50 +//#define JOULES_PER_WATT_HOUR 3600 // may be needed for datalogging +#define WORKING_ZONE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator +#define NO_OF_PHASES 3 +#define DATALOG_PERIOD_IN_SECONDS 10 + +const byte noOfDumploads = 3; + +enum polarities {NEGATIVE, POSITIVE}; +enum outputModes {ANTI_FLICKER, NORMAL}; +enum loadPriorityModes {LOAD_1_HAS_PRIORITY, LOAD_0_HAS_PRIORITY}; + +// enum loadStates {LOAD_ON, LOAD_OFF}; // for use if loads are active low (original PCB) +enum loadStates {LOAD_OFF, LOAD_ON}; // for use if loads are active high (Rev 2 PCB) +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +// For this multi-load version, the same mechanism has been retained but the +// output mode is hard-coded as below: +enum outputModes outputMode = ANTI_FLICKER; + +// In this multi-load version, the external switch is re-used to determine the load priority +enum loadPriorityModes loadPriorityMode = LOAD_0_HAS_PRIORITY; + +#ifdef RF_PRESENT +#define freq RF12_433MHZ // Use the freq to match the module you have. + +const int nodeID = 10; +const int networkGroup = 210; +const int UNO = 1; +#endif + +typedef struct { + int power_L1; + int power_L2; + int power_L3; + int Vrms_L1; + int Vrms_L2; + int Vrms_L3;} Tx_struct; // revised data for RF comms +Tx_struct tx_data; + + +// ----------- Pinout assignments ----------- +// +// digital pins: +const byte loadPrioritySelectorPin = 3; // // for 3-phase PCB +// D4 is not in use +const byte physicalLoad_0_pin = 5; // for 3-phase PCB, Load #1 (Rev 2 PCB) +const byte physicalLoad_1_pin = 6; // for 3-phase PCB, Load #2 (Rev 2 PCB) +const byte physicalLoad_2_pin = 7; // for 3-phase PCB, Load #3 (Rev 2 PCB) +// D8 is not in use +// D9 is not in use + +// analogue input pins +const byte sensorV[NO_OF_PHASES] = {0,2,4}; // for 3-phase PCB +const byte sensorI[NO_OF_PHASES] = {1,3,5}; // for 3-phase PCB + + +// -------------- general global variables ----------------- +// +// Some of these variables are used in multiple blocks so cannot be static. +// For integer maths, some variables need to be 'long' +// +boolean beyondStartUpPeriod = false; // start-up delay, allows things to settle +byte initialDelay = 3; // in seconds, to allow time to open the Serial monitor +byte startUpPeriod = 3; // in seconds, to allow LP filter to settle + +long DCoffset_V_long[NO_OF_PHASES]; // <--- for LPF +long DCoffset_V_min; // <--- for LPF (min limit) +long DCoffset_V_max; // <--- for LPF (max limit) +int DCoffset_I_nom; // nominal mid-point value of ADC @ x1 scale + +// for 3-phase use, with units of Joules * CYCLES_PER_SECOND +float capacityOfEnergyBucket_main; +float energyInBucket_main; +float midPointOfEnergyBucket_main; +float lowerThreshold_default; +float lowerEnergyThreshold; +float upperThreshold_default; +float upperEnergyThreshold; +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.4 + +// for improved control of multiple loads +boolean recentTransition = false; +byte postTransitionCount; +#define POST_TRANSITION_MAX_COUNT 3 // <-- allows each transition to take effect +//#define POST_TRANSITION_MAX_COUNT 50 // <-- for testing only +byte activeLoad = 0; + +// for datalogging +int datalogCountInMainsCycles; +const int maxDatalogCountInMainsCycles = DATALOG_PERIOD_IN_SECONDS * CYCLES_PER_SECOND; +float energyStateOfPhase[NO_OF_PHASES]; // only used for datalogging + +// for interaction between the main processor and the ISR +volatile boolean dataReady = false; +volatile int sampleV[NO_OF_PHASES]; +volatile int sampleI[NO_OF_PHASES]; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int mainsCycles_forContinuityChecker; +int lowestNoOfSampleSetsPerMainsCycle; + +// Calibration values +//------------------- +// Three calibration values are used in this sketch: powerCal, phaseCal and voltageCal. +// With most hardware, the default values are likely to work fine without +// need for change. A compact explanation of each of these values now follows: + +// When calculating real power, which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal[NO_OF_PHASES] = {0.043, 0.043, 0.043}; + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// NB. Any tool which determines the optimal value of phaseCal must have a similar +// scheme for taking sample values as does this sketch. +// +const float phaseCal[NO_OF_PHASES] = {0.5, 0.5, 0.5}; // <- nominal values only +int phaseCal_int[NO_OF_PHASES]; // to avoid the need for floating-point maths + +// For datalogging purposes, voltageCal has been added too. Because the range of ADC values is +// similar to the actual range of volts, the optimal value for this cal factor is likely to be +// close to unity. +const float voltageCal[NO_OF_PHASES] = {1.03, 1.03, 1.03}; // compared with Fluke 77 meter + + + + +void setup() +{ + delay (initialDelay * 1000); // allows time to open the Serial Monitor + + Serial.begin(9600); // initialize Serial interface + Serial.println(); + Serial.println(); + Serial.println(); + Serial.println("----------------------------------"); + Serial.println("Sketch ID: Mk2_3phase_RFdatalog_4.ino"); + + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for Load #1 + pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for Load #2 + pinMode(physicalLoad_2_pin, OUTPUT); // driver pin for Load #3 + + for(int i = 0; i< noOfDumploads; i++) + { + logicalLoadState[i] = LOAD_OFF; + } + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // update the local load's state. + digitalWrite(physicalLoad_1_pin, physicalLoadState[1]); // update the additional load state (inverse logic). + digitalWrite(physicalLoad_2_pin, physicalLoadState[2]); // update the additional load state (inverse logic). + + pinMode(loadPrioritySelectorPin, INPUT); + digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and + loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority mode + + for (byte phase = 0; phase < NO_OF_PHASES; phase++) + { + // When using integer maths, calibration values that have been supplied in + // floating point form need to be rescaled. + phaseCal_int[phase] = phaseCal[phase] * 256; // for integer maths + DCoffset_V_long[phase] = 512L * 256; // nominal mid-point value of ADC @ x256 scale + } + + // Define operating limits for the LP filters which identify DC offset in the voltage + // sample streams. By limiting the output range, these filters always should start up + // correctly. + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + DCoffset_I_nom = 512; // nominal mid-point value of ADC @ x1 scale + + // for the main energy bucket + capacityOfEnergyBucket_main = (float)WORKING_ZONE_IN_JOULES * CYCLES_PER_SECOND; + midPointOfEnergyBucket_main = capacityOfEnergyBucket_main * 0.5; // for resetting flexible thresholds + energyInBucket_main = 0; + + Serial.println ("ADC mode: free-running"); + Serial.print ("requiredExport in Watts = "); + Serial.println (REQUIRED_EXPORT_IN_WATTS); + + // Set up the ADC to be free-running + ADCSRA = (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples (3 pairs). The main processor and +// the ADC work autonomously, their operation being synchnonised only via the dataReady flag. +// +void processRawSamples() +{ + static long sumP[NO_OF_PHASES]; + static enum polarities polarityOfLastSampleV[NO_OF_PHASES]; // for zero-crossing detection + static long lastSampleV_minusDC_long[NO_OF_PHASES]; // for the phaseCal algorithm + static long cumVdeltasThisCycle_long[NO_OF_PHASES]; // for the LPF which determines DC offset (voltage) + static int samplesDuringThisMainsCycle[NO_OF_PHASES]; + static long sum_Vsquared[NO_OF_PHASES]; + static long samplesDuringThisDatalogPeriod; + enum polarities polarityNow; + + // The raw V and I samples are processed in "phase pairs" + for (byte phase = 0; phase < NO_OF_PHASES; phase++) + { + // remove DC offset from each raw voltage sample by subtracting the accurate value + // as determined by its associated LP filter. + long sampleV_minusDC_long = ((long)sampleV[phase]<<8) - DCoffset_V_long[phase]; + + // determine polarity, to aid the logical flow + if(sampleV_minusDC_long > 0) { + polarityNow = POSITIVE; } + else { + polarityNow = NEGATIVE; } + + if (polarityNow == POSITIVE) + { + if (polarityOfLastSampleV[phase] != POSITIVE) + { + if (beyondStartUpPeriod) + { + // This is the start of a new +ve half cycle, for this phase, just after the + // zero-crossing point. Before the contribution from this phase can be added + // to the running total, the cal factor for this phase must be applied. + // + float realPower = (sumP[phase] / samplesDuringThisMainsCycle[phase]) * powerCal[phase]; + + processLatestContribution(phase, realPower); // runs at 6.6 ms intervals + + // A performance check to monitor and display the minimum number of sets of + // ADC samples per mains cycle, the expected number being 20ms / (104us * 6) = 32.05 + // + if (phase == 0) + { + if (samplesDuringThisMainsCycle[phase] < lowestNoOfSampleSetsPerMainsCycle) + { + lowestNoOfSampleSetsPerMainsCycle = samplesDuringThisMainsCycle[phase]; + } + mainsCycles_forContinuityChecker++; + if (mainsCycles_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + mainsCycles_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + } + + sumP[phase] = 0; + samplesDuringThisMainsCycle[phase] = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (initialDelay + startUpPeriod) * 1000) + { + beyondStartUpPeriod = true; + mainsCycles_forContinuityChecker = 1; // opportunity has been missed for this cycle + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + // check to see whether the trigger device can now be reliably armed + if ((phase == 0) && samplesDuringThisMainsCycle[0] == 2) // lower value for larger sample set + { + if (beyondStartUpPeriod) + { + // This code is executed once per 20mS, shortly after the start of each new + // mains cycle on phase 0. + // + datalogCountInMainsCycles++; + + // Changling the state of the loads is is a 3-part process: + // - change the LOGICAL load states as necessary to maintain the energy level + // - update the PHYSICAL load states according to the logical -> physical mapping + // - update the driver lines for each of the loads. + // + // Restrictions apply for the period immediately after a load has been switched. + // Here the recentTransition flag is checked and updated as necessary. + if (recentTransition) + { + postTransitionCount++; + if (postTransitionCount >= POST_TRANSITION_MAX_COUNT) + { + recentTransition = false; + } + } + + if (energyInBucket_main > midPointOfEnergyBucket_main) + { + // the energy state is in the upper half of the working range + lowerEnergyThreshold = lowerThreshold_default; // reset the "opposite" threshold + if (energyInBucket_main > upperEnergyThreshold) + { + // Because the energy level is high, some action may be required + boolean OK_toAddLoad = true; + byte tempLoad = nextLogicalLoadToBeAdded(); + if (tempLoad < noOfDumploads) + { + // a load which is now OFF has been identified for potentially being switched ON + if (recentTransition) + { + // During the post-transition period, any increase in the energy level is noted. + upperEnergyThreshold = energyInBucket_main; + + // the energy thresholds must remain within range + if (upperEnergyThreshold > capacityOfEnergyBucket_main) + { + upperEnergyThreshold = capacityOfEnergyBucket_main; + } + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toAddLoad = false; + } + } + + if (OK_toAddLoad) + { + logicalLoadState[tempLoad] = LOAD_ON; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + Serial.print('+'); + Serial.println(activeLoad); + } + } + } + } + else + { // the energy state is in the lower half of the working range + upperEnergyThreshold = upperThreshold_default; // reset the "opposite" threshold + if (energyInBucket_main < lowerEnergyThreshold) + { + // Because the energy level is low, some action may be required + boolean OK_toRemoveLoad = true; + byte tempLoad = nextLogicalLoadToBeRemoved(); + if (tempLoad < noOfDumploads) + { + // a load which is now ON has been identified for potentially being switched OFF + if (recentTransition) + { + // During the post-transition period, any decrease in the energy level is noted. + lowerEnergyThreshold = energyInBucket_main; + + // the energy thresholds must remain within range + if (lowerEnergyThreshold < 0) + { + lowerEnergyThreshold = 0; + } + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toRemoveLoad = false; + } + } + + if (OK_toRemoveLoad) + { + logicalLoadState[tempLoad] = LOAD_OFF; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + Serial.print('-'); + Serial.println(activeLoad); + } + } + } + } + + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + // update the control ports for each of the physical loads + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger + digitalWrite(physicalLoad_1_pin, physicalLoadState[1]); // active low for trigger + digitalWrite(physicalLoad_2_pin, physicalLoadState[2]); // active low for trigger + + // Now that the energy-related decisions have been taken, min and max limits can now + // be applied to the level of the energy bucket. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_main > capacityOfEnergyBucket_main) { + energyInBucket_main = capacityOfEnergyBucket_main; } + else + if (energyInBucket_main < 0) { + energyInBucket_main = 0; } + + if (datalogCountInMainsCycles >= maxDatalogCountInMainsCycles) + { + datalogCountInMainsCycles = 0; + + // To provide sufficient range for a dataloging period of at least 20 seconds, the accumulator + // for Vsquared is now scaled at 1/16 of its previous V_ADC * V_ADC value. + // Hence the * 4 factor that appears below after the sqrt() operation below. + // + tx_data.power_L1 = energyStateOfPhase[0] / maxDatalogCountInMainsCycles; + tx_data.power_L1 *= -1; // to match the OEM convention (import is =ve; export is -ve) + tx_data.power_L2 = energyStateOfPhase[1] / maxDatalogCountInMainsCycles; + tx_data.power_L2 *= -1; // to match the OEM convention (import is =ve; export is -ve) + tx_data.power_L3 = energyStateOfPhase[2] / maxDatalogCountInMainsCycles; + tx_data.power_L3 *= -1; // to match the OEM convention (import is =ve; export is -ve) + tx_data.Vrms_L1 = (int)(voltageCal[0] * sqrt(sum_Vsquared[0] / samplesDuringThisDatalogPeriod) * 4); + tx_data.Vrms_L2 = (int)(voltageCal[1] * sqrt(sum_Vsquared[1] / samplesDuringThisDatalogPeriod) * 4); + tx_data.Vrms_L3 = (int)(voltageCal[2] * sqrt(sum_Vsquared[2] / samplesDuringThisDatalogPeriod) * 4); +#ifdef RF_PRESENT + send_rf_data(); +#endif +/* <-- Warning - Unlike its 1-phase equivalent, this 3-phase code can be affected by Serial statements! + Serial.print(energyInBucket_main / CYCLES_PER_SECOND); + Serial.print(", "); + Serial.println(tx_data.power_L1); + Serial.print(", "); + Serial.print(tx_data.power_L2); + Serial.print(", "); + Serial.print(tx_data.power_L3); + Serial.print(", "); + Serial.println(tx_data.Vrms_L1); + Serial.print(", "); + Serial.print(tx_data.Vrms_L2); + Serial.print(", "); + Serial.println(tx_data.Vrms_L3); +*/ + energyStateOfPhase[0] = 0; + energyStateOfPhase[1] = 0; + energyStateOfPhase[2] = 0; + sum_Vsquared[0] = 0; + sum_Vsquared[1] = 0; + sum_Vsquared[2] = 0; + samplesDuringThisDatalogPeriod = 0; + + +/* <-- Warning - Unlike its 1-phase equivalent, this 3-phase code can be affected by Serial statements! + for (int i = 0; i < noOfDumploads; i++) + { + Serial.print(logicalLoadState[i]); + } + Serial.println(); +*/ + } + } + } + } // end of processing that is specific to samples where the voltage is positive + + else // the polarity of this sample is negative + { + if (polarityOfLastSampleV[phase] != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // This is a convenient point to update the Low Pass Filter for removing the DC + // component from the phase that is being processed. + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + DCoffset_V_long[phase] += (cumVdeltasThisCycle_long[phase]>>12); + cumVdeltasThisCycle_long[phase] = 0; + + // To ensure that this LP filter will always start up correctly when 240V AC is + // available, its output value needs to be prevented from drifting beyond the likely range + // of the voltage signal. + // + if (DCoffset_V_long[phase] < DCoffset_V_min) { + DCoffset_V_long[phase] = DCoffset_V_min; } + else + if (DCoffset_V_long[phase] > DCoffset_V_max) { + DCoffset_V_long[phase] = DCoffset_V_max; } + + if (phase == 0) + { + checkLoadPrioritySelection(); // updates load priorities if the switch is changed + } + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is negative + + // Processing for EVERY pair of samples. Most of this code is not used during the + // start-up period, but it does no harm to leave it in place. Accumulated values + // are cleared when the beyondStartUpPhase flag is set to true. + // + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_long = ((long)(sampleI[phase] - DCoffset_I_nom))<<8; + + // phase-shift the voltage waveform so that it aligns with the current when a + // resistive load is used + long phaseShiftedSampleV_minusDC_long = lastSampleV_minusDC_long[phase] + + (((sampleV_minusDC_long - lastSampleV_minusDC_long[phase])*phaseCal_int[phase])>>8); + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleV_minusDC_long>>2; // reduce to 16-bits (x64, or 2^6) + long filtI_div4 = sampleIminusDC_long>>2; // reduce to 16-bits (x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (x4096, or 2^12) + instP = instP>>12; // reduce to 20-bits (x1) + sumP[phase] +=instP; // scaling is x1 + + // for the Vrms calculation (for datalogging only) + long inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (x4096, or 2^12) + // inst_Vsquared = inst_Vsquared>>12; // 20-bits (x1), not enough range :-( + inst_Vsquared = inst_Vsquared>>16; // 16-bits (x1/16, or 2^-4), for more datalog range :-) + sum_Vsquared[phase] += inst_Vsquared; // scaling is x1/16 + if (phase == 0) { + samplesDuringThisDatalogPeriod ++; } // no need to keep separate counts for each phase + + // general housekeeping + cumVdeltasThisCycle_long[phase] += sampleV_minusDC_long; // for use with LP filter + samplesDuringThisMainsCycle[phase] ++; + + // store items for use during next loop + lastSampleV_minusDC_long[phase] = sampleV_minusDC_long; // required for phaseCal algorithm + polarityOfLastSampleV[phase] = polarityNow; // for identification of half cycle boundaries + } +} +// end of processRawSamples() + + +void processLatestContribution(byte phase, float power) +{ + float latestEnergyContribution = power; // for efficiency, the energy scale is Joules * CYCLES_PER_SECOND + + // add the latest energy contribution to the relevant per-phase accumulator + // (only used for datalogging of power) + energyStateOfPhase[phase] += latestEnergyContribution; + + // add the latest energy contribution to the main energy accumulator + energyInBucket_main += latestEnergyContribution; + + // apply any adjustment that is required. + if (phase == 0) + { + energyInBucket_main -= REQUIRED_EXPORT_IN_WATTS; // energy scale is Joules x 50 + } + + // Applying max and min limits to the main accumulator's level + // is deferred until after the energy related decisions have been taken + // +} + +byte nextLogicalLoadToBeAdded() +{ + byte retVal = noOfDumploads; + boolean success = false; + + for (byte index = 0; index < noOfDumploads && !success; index++) + { + if (logicalLoadState[index] == LOAD_OFF) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +byte nextLogicalLoadToBeRemoved() +{ + byte retVal = noOfDumploads; + boolean success = false; + + // NB. the index cannot be a 'byte' because the loop would not terminate correctly! + for (char index = (noOfDumploads -1); index >= 0 && !success; index--) + { + if (logicalLoadState[index] == LOAD_ON) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * The association between the physical and logical loads is 1:1. By default, numerical + * equivalence is maintained, so logical(N) maps to physical(N). If physical load 1 is set + * to have priority, rather than physical load 0, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * Any other mapping relaionships could be configured here. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } + + if (loadPriorityMode == LOAD_1_HAS_PRIORITY) + { + // swap physical loads 0 & 1 if remote load has priority + physicalLoadState[0] = logicalLoadState[1]; + physicalLoadState[1] = logicalLoadState[0]; + } +} + + +// this function changes the value of the load priorities if the state of the external switch is altered +void checkLoadPrioritySelection() +{ + static byte loadPrioritySwitchCcount = 0; + int pinState = digitalRead(loadPrioritySelectorPin); + if (pinState != loadPriorityMode) + { + loadPrioritySwitchCcount++; + } + if (loadPrioritySwitchCcount >= 20) + { + loadPrioritySwitchCcount = 0; + loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable + Serial.print ("loadPriority selection changed to "); + if (loadPriorityMode == LOAD_0_HAS_PRIORITY) { + Serial.println ( "load 0"); } + else { + Serial.println ( "load 1"); } + } +} + + +// Although this sketch always operates in ANTI_FLICKER mode, it was convenient +// to leave this mechanism in place. +// +void configureParamsForSelectedOutputMode() +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerThreshold_default = + capacityOfEnergyBucket_main * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperThreshold_default = + capacityOfEnergyBucket_main * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerThreshold_default = capacityOfEnergyBucket_main * 0.5; + upperThreshold_default = capacityOfEnergyBucket_main * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_main = "); + Serial.println(capacityOfEnergyBucket_main); + Serial.print(" lowerEnergyThreshold = "); + Serial.println(lowerThreshold_default); + Serial.print(" upperEnergyThreshold = "); + Serial.println(upperThreshold_default); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + + +#ifdef RF_PRESENT +void send_rf_data() +{ + int i = 0; + while (!rf12_canSend() && i<10) + { + rf12_recvDone(); + i++; + } + rf12_sendStart(0, &tx_data, sizeof tx_data); +} +#endif + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + + + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_1.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_1.ino new file mode 100644 index 0000000..cda0398 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_1.ino @@ -0,0 +1,1052 @@ +/* Mk2_RF_datalog_1.ino + * + * This sketch is for diverting surplus PV power to a dump load using a triac. + * Routine dataloogig is also supported using the on-board RFM12B module. + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The integral voltage sensor is fed from one of the secondary coils of the + * transformer. Current is measured via Current Transformers at the + * CT1 and CT2 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. When the RFM12B module is + * in use, the display can only be used in conjunction with an extra pair of + * logic chips. These are ICs 3 and 4, which reduce the number of processor pins + * that are needed to drive the display. + * + * This sketch is based on the Mk2i PV Router code that I have posted in on the + * OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * Robin Emley + * www.Mk2PVrouter.co.uk + * April 2014 + */ + +#include +#include // JeeLib is available at from: http://github.com/jcw/jeelib +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define SWEETZONE_IN_JOULES 3600 +#define RF_SEND_PERIOD 2 // seconds + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum triacStates {TRIAC_ON, TRIAC_OFF}; // the external trigger device is active low +enum outputModes {ANTI_FLICKER, NORMAL}; + +// ---------------- Extra Features selection ---------------------- +// +// - WORKLOAD_CHECK, for determining how much spare processing time there is. +// +// #define WORKLOAD_CHECK // <-- Include this line is this feature is required + + +// The power-diversion logic can operate in either of two modes: +// +// - NORMAL, where the triac switches rapidly on/off to maintain a constant energy level. +// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations +// of the local mains voltage. +// +// The output mode is determined in realtime via a selector switch +enum outputModes outputMode; + +/* frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ + */ +#define freq RF12_433MHZ // Use the freq to match the module you have. + +const int nodeID = 10; // RFM12B node ID +const int networkGroup = 210; // RFM12B wireless network group - needs to be same as emonBase and emonGLCD +const int UNO = 1; // Set to 0 if you're not using the UNO bootloader (i.e using Duemilanove) + // - All Atmega's shipped from OpenEnergyMonitor come with Arduino Uno bootloader + +int messageNumber = 0; + +// data structure for RF comms +typedef struct { + int msgNumber; + int powerAtSupplyPoint; + int divertedEnergyTotal; +} Tx_struct; +Tx_struct tx_data; // an instance of this structure type + + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +// D2 is for the RFM12B +const byte outputModeSelectorPin = 3; // <-- with the internal pullup +const byte outputForTrigger = 4; +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +// D10 is for the RFM12B +// D11 is for the RFM12B +// D12 is for the RFM12B +// D13 is for the RFM12B + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + + +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long cycleCount = 0; // counts mains cycles from start-up +long triggerThreshold_long; // for determining when the trigger may be safely armed +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long lowerEnergyThreshold_long; // for turning triac off +long upperEnergyThreshold_long; // for turning triac on +// int phaseCal_grid_int; // to avoid the need for floating-point maths +// int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; +long mainsCyclesPerHour; + +// this setting is only used if anti-flicker mode is enabled +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.5 + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +int sampleI_grid; +int sampleI_diverted; +int sampleV; + + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// With most hardware, the default values are likely to work fine without +// need for change. A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When +// measuring a resistive load, the voltage and current waveforms should be perfectly +// aligned. In this situation, the Power Factor will be 1. +// +// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct +// value of phaseCal for any hardware configuration. An index of my various Mk2-related +// exhibits is available at http://openenergymonitor.org/emon/node/1757 +// +//const float phaseCal_grid = 1.0; <--- not used in this version +//const float phaseCal_diverted = 1.0; <--- not used in this version + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 10 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. In this version, +// the decimal point has to be treated differently than the other seven segments, so +// a convenient means of accessing this column is provided. +// +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // energy diversion detection + + +void setup() +{ + pinMode(outputForTrigger, OUTPUT); + digitalWrite (outputForTrigger, TRIAC_OFF); // the external trigger is active low + + pinMode(outputModeSelectorPin, INPUT); + digitalWrite(outputModeSelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(outputModeSelectorPin); // initial selection and + outputMode = (enum outputModes)pinState; // assignment of output mode + + delay(5000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_RFdatalog_1.ino"); + Serial.println(); + + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // control lines for the 74HC4543 7-seg display driver and the DP line + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + // control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + + + // When using integer maths, calibration values that have supplied in floating point + // form need to be rescaled. + // +// phaseCal_grid_int = phaseCal_grid * 256; // for integer maths +// phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + configureParamsForSelectedOutputMode(); + + Serial.println ("----"); + +#ifdef WORKLOAD_CHECK + Serial.println ("WELCOME TO WORKLOAD_CHECK "); + +// <<- start of commented out section, to save on RAM space! +/* + Serial.println (" This mode of operation allows the spare processing capacity of the system"); + Serial.println ("to be analysed. Additional delay is gradually increased until all spare time"); + Serial.println ("has been used up. This value (in uS) is noted and the process is repeated. "); + Serial.println ("The delay setting is increased by 1uS at a time, and each value of delay is "); + Serial.println ("checked several times before the delay is increased. "); + */ +// <<- end of commented out section, to save on RAM space! + + Serial.println (" The displayed value is the amount of spare time, per set of V & I samples, "); + Serial.println ("that is available for doing additional processing."); + Serial.println (); + #endif + + rf12_initialize(nodeID, freq, networkGroup); // initialize RF + rf12_sleep(RF12_SLEEP); + +} + +// An Interrupt Service Routine is now defined in which the ADC is instructed to +// measure V and I alternately. A "data ready"flag is set after each voltage conversion +// has been completed. +// For each pair of samples, this means that current is measured before voltage. The +// current sample is taken first because the phase of the waveform for current is generally +// slightly advanced relative to the waveform for voltage. The data ready flag is cleared +// within loop(). +// This Interrupt Service Routine is for use when the ADC is fixed timer mode. It is +// executed whenever the ADC timer expires. In this mode, the next ADC conversion is +// initiated from within this ISR. +// +void timerIsr(void) +{ + static unsigned char sample_index = 0; + + switch(sample_index) + { + case 0: + sampleV = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current + ADCSRA |= (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples. The main processor and +// the ADC work autonomously, their operation being only linked via the dataReady flag. +// As soon as a new set of data is made available by the ADC, the main processor can +// start to work on it immediately. +// +void allGeneralProcessing() +{ + static boolean triggerNeedsToBeArmed = false; // once per mains cycle (+ve half) + static int samplesDuringThisCycle; // for normalising the power in each mains cycle + static long sumP_grid; // for per-cycle summation of 'real power' + static long sumP_diverted; // for per-cycle summation of 'real power' + static enum polarities polarityOfLastSampleV; // for zero-crossing detection + static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) + static long lastSampleVminusDC_long; // for the phaseCal algorithm + static byte perSecondCounter = 0; + static enum triacStates nextStateOfTriac = TRIAC_OFF; + + // extra items for datalogging + static long sumP_atSupplyPoint; + static unsigned int samplesDuringDatalogPeriod; + static int RF_send_counter = 0; // counts seconds + + // remove DC offset from the raw voltage sample by subtracting the accurate value + // as determined by a LP filter. + long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long; + + // determine the polarity of the latest voltage sample + enum polarities polarityNow; + if(sampleVminusDC_long > 0) { + polarityNow = POSITIVE; } + else { + polarityNow = NEGATIVE; } + + if (polarityNow == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + cycleCount++; + + triggerNeedsToBeArmed = true; // the trigger is armed once during each +ve half-cycle + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / samplesDuringThisCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / samplesDuringThisCycle; // proportional to Watts + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. + + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) { + realEnergy_diverted = 0; } // to avoid 'creep' + + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + perSecondCounter++; + if(perSecondCounter >= CYCLES_PER_SECOND) + { + perSecondCounter = 0; +/* + Serial.print("Diverted: " ); + Serial.print(divertedEnergyTotal_Wh); + Serial.print(" Wh plus "); + Serial.print((powerCal_diverted / CYCLES_PER_SECOND) * divertedEnergyRecent_IEU); + + Serial.print(" J , EDD is" ); +*/ + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } +/* + if (EDD_isActive) { + Serial.println(" on" ); } + else { + Serial.println(" off" ); } +*/ + configureValueForDisplay(); // occurs every second + + // routine data is to be transmitted every N seconds + RF_send_counter++; + if (RF_send_counter >= RF_SEND_PERIOD) + { + RF_send_counter = 0; + + // calculate the average power at the supply point + long realPower_long = sumP_atSupplyPoint / samplesDuringDatalogPeriod; + tx_data.powerAtSupplyPoint = realPower_long * powerCal_grid; + sumP_atSupplyPoint = 0; + samplesDuringDatalogPeriod = 0; + + tx_data.msgNumber = messageNumber; + tx_data.divertedEnergyTotal = divertedEnergyTotal_Wh; + send_rf_data(); + Serial.print(tx_data.msgNumber); + Serial.print(", "); + Serial.print(tx_data.powerAtSupplyPoint); + Serial.print(", "); + Serial.println(tx_data.divertedEnergyTotal); + messageNumber++; + } + } + + // clear the per-cycle accumulators for use in this new mains cycle. + samplesDuringThisCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + if (triggerNeedsToBeArmed == true) + { + // check to see whether the trigger device can now be reliably armed + if (samplesDuringThisCycle == 3) // much easier than checking the voltage level + { + if (energyInBucket_long < lowerEnergyThreshold_long) { + // when below the lower threshold, always set the triac to "off" + nextStateOfTriac = TRIAC_OFF; } + else + if (energyInBucket_long > upperEnergyThreshold_long) { + // when above the upper threshold, always set the triac to "off" + nextStateOfTriac = TRIAC_ON; } + else { + // otherwise, leave the triac's state unchanged (hysteresis) + } + + // set the Arduino's output pin accordingly, and clear the flag + digitalWrite(outputForTrigger, nextStateOfTriac); + triggerNeedsToBeArmed = false; + + // update the Energy Diversion Detector + if (nextStateOfTriac == TRIAC_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + } + } + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > startUpPeriod * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + samplesDuringThisCycle = 0; + Serial.println ("Go!"); + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + checkOutputModeSelection(); // updates outputMode if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is positive + + // processing for EVERY pair of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform +// long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + sumP_atSupplyPoint +=instP; // cumulative power, for datalogging + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform +// long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + samplesDuringThisCycle++; + samplesDuringDatalogPeriod++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityOfLastSampleV = polarityNow; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + + +// this function changes the value of outputMode if the state of the external switch is altered +void checkOutputModeSelection() +{ + static byte count = 0; + int pinState = digitalRead(outputModeSelectorPin); + if (pinState != outputMode) + { + count++; + } + if (count >= 20) + { + count = 0; + outputMode = (enum outputModes)pinState; // change the global variable + Serial.print ("outputMode selection changed to "); + if (outputMode == NORMAL) { + Serial.println ( "normal"); } + else { + Serial.println ( "anti-flicker"); } + + configureParamsForSelectedOutputMode(); + } +} + + +void configureParamsForSelectedOutputMode() +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + upperEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" lowerEnergyThreshold_long = "); + Serial.println(lowerEnergyThreshold_long); + Serial.print(" upperEnergyThreshold_long = "); + Serial.println(upperEnergyThreshold_long); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + +// Serial.println(divertedEnergyTotal_Wh); + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +// valueToBeDisplayed++; +} + + + + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + +void send_rf_data() +{ + rf12_sleep(RF12_WAKEUP); + // if ready to send + exit route if it gets stuck + int i = 0; + while (!rf12_canSend() && i<10) + { + rf12_recvDone(); + i++; + } + rf12_sendStart(0, &tx_data, sizeof tx_data); + rf12_sendWait(2); + rf12_sleep(RF12_SLEEP); +} + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_2.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_2.ino new file mode 100644 index 0000000..a7da0ff --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_2.ino @@ -0,0 +1,1068 @@ +/* Mk2_RF_datalog_2.ino + * + * This sketch is for diverting surplus PV power to a dump load using a triac. + * Routine dataloogig is also supported using the on-board RFM12B module. + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The integral voltage sensor is fed from one of the secondary coils of the + * transformer. Current is measured via Current Transformers at the + * CT1 and CT2 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. When the RFM12B module is + * in use, the display can only be used in conjunction with an extra pair of + * logic chips. These are ICs 3 and 4, which reduce the number of processor pins + * that are needed to drive the display. + * + * This sketch is based on the Mk2i PV Router code that I have posted in on the + * OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * Robin Emley + * www.Mk2PVrouter.co.uk + * April 2014 + */ + +#include +#include // JeeLib is available at from: http://github.com/jcw/jeelib +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define SWEETZONE_IN_JOULES 3600 +#define RF_SEND_PERIOD 2 // seconds +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum triacStates {TRIAC_ON, TRIAC_OFF}; // the external trigger device is active low +enum outputModes {ANTI_FLICKER, NORMAL}; + +// ---------------- Extra Features selection ---------------------- +// +// - WORKLOAD_CHECK, for determining how much spare processing time there is. +// +// #define WORKLOAD_CHECK // <-- Include this line is this feature is required + + +// The power-diversion logic can operate in either of two modes: +// +// - NORMAL, where the triac switches rapidly on/off to maintain a constant energy level. +// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations +// of the local mains voltage. +// +// The output mode is determined in realtime via a selector switch +enum outputModes outputMode; + +/* frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ + */ +#define freq RF12_868MHZ // Use the freq to match the module you have. + +const int nodeID = 10; // RFM12B node ID +const int networkGroup = 210; // RFM12B wireless network group - needs to be same as emonBase and emonGLCD +const int UNO = 1; // Set to 0 if you're not using the UNO bootloader (i.e using Duemilanove) + // - All Atmega's shipped from OpenEnergyMonitor come with Arduino Uno bootloader + +int messageNumber = 0; + +// data structure for RF comms +typedef struct { +// int msgNumber; + int powerAtSupplyPoint; // import = +ve, to match OEM convention + int divertedEnergyTotal; // always positive +} Tx_struct; +Tx_struct tx_data; // an instance of this structure type + + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +// D2 is for the RFM12B +const byte outputModeSelectorPin = 3; // <-- with the internal pullup +const byte outputForTrigger = 4; +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +// D10 is for the RFM12B +// D11 is for the RFM12B +// D12 is for the RFM12B +// D13 is for the RFM12B + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + + +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long cycleCount = 0; // counts mains cycles from start-up +long triggerThreshold_long; // for determining when the trigger may be safely armed +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long lowerEnergyThreshold_long; // for turning triac off +long upperEnergyThreshold_long; // for turning triac on +// int phaseCal_grid_int; // to avoid the need for floating-point maths +// int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; +long mainsCyclesPerHour; + +// this setting is only used if anti-flicker mode is enabled +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.5 + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +int sampleI_grid; +int sampleI_diverted; +int sampleV; + + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// With most hardware, the default values are likely to work fine without +// need for change. A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When +// measuring a resistive load, the voltage and current waveforms should be perfectly +// aligned. In this situation, the Power Factor will be 1. +// +// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct +// value of phaseCal for any hardware configuration. An index of my various Mk2-related +// exhibits is available at http://openenergymonitor.org/emon/node/1757 +// +//const float phaseCal_grid = 1.0; <--- not used in this version +//const float phaseCal_diverted = 1.0; <--- not used in this version + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 10 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. In this version, +// the decimal point has to be treated differently than the other seven segments, so +// a convenient means of accessing this column is provided. +// +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // energy diversion detection +long requiredExportPerMainsCycle_inIEU; +float IEUtoJoulesConversion_CT1; + + +void setup() +{ + pinMode(outputForTrigger, OUTPUT); + digitalWrite (outputForTrigger, TRIAC_OFF); // the external trigger is active low + + pinMode(outputModeSelectorPin, INPUT); + digitalWrite(outputModeSelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(outputModeSelectorPin); // initial selection and + outputMode = (enum outputModes)pinState; // assignment of output mode + + delay(5000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_RFdatalog_2.ino"); + Serial.println(); + + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // control lines for the 74HC4543 7-seg display driver and the DP line + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + // control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + + + // When using integer maths, calibration values that have supplied in floating point + // form need to be rescaled. + // +// phaseCal_grid_int = phaseCal_grid * 256; // for integer maths +// phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + configureParamsForSelectedOutputMode(); + + Serial.println ("----"); + +#ifdef WORKLOAD_CHECK + Serial.println ("WELCOME TO WORKLOAD_CHECK "); + +// <<- start of commented out section, to save on RAM space! +/* + Serial.println (" This mode of operation allows the spare processing capacity of the system"); + Serial.println ("to be analysed. Additional delay is gradually increased until all spare time"); + Serial.println ("has been used up. This value (in uS) is noted and the process is repeated. "); + Serial.println ("The delay setting is increased by 1uS at a time, and each value of delay is "); + Serial.println ("checked several times before the delay is increased. "); + */ +// <<- end of commented out section, to save on RAM space! + + Serial.println (" The displayed value is the amount of spare time, per set of V & I samples, "); + Serial.println ("that is available for doing additional processing."); + Serial.println (); + #endif + + rf12_initialize(nodeID, freq, networkGroup); // initialize RF +// rf12_sleep(RF12_SLEEP); <- the RFM12B now stays awake throughout + +} + +// An Interrupt Service Routine is now defined in which the ADC is instructed to +// measure V and I alternately. A "data ready"flag is set after each voltage conversion +// has been completed. +// For each pair of samples, this means that current is measured before voltage. The +// current sample is taken first because the phase of the waveform for current is generally +// slightly advanced relative to the waveform for voltage. The data ready flag is cleared +// within loop(). +// This Interrupt Service Routine is for use when the ADC is fixed timer mode. It is +// executed whenever the ADC timer expires. In this mode, the next ADC conversion is +// initiated from within this ISR. +// +void timerIsr(void) +{ + static unsigned char sample_index = 0; + + switch(sample_index) + { + case 0: + sampleV = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current + ADCSRA |= (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples. The main processor and +// the ADC work autonomously, their operation being only linked via the dataReady flag. +// As soon as a new set of data is made available by the ADC, the main processor can +// start to work on it immediately. +// +void allGeneralProcessing() +{ + static boolean triggerNeedsToBeArmed = false; // once per mains cycle (+ve half) + static int samplesDuringThisCycle; // for normalising the power in each mains cycle + static long sumP_grid; // for per-cycle summation of 'real power' + static long sumP_diverted; // for per-cycle summation of 'real power' + static enum polarities polarityOfLastSampleV; // for zero-crossing detection + static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) + static long lastSampleVminusDC_long; // for the phaseCal algorithm + static byte perSecondCounter = 0; + static enum triacStates nextStateOfTriac = TRIAC_OFF; + + // extra items for datalogging + static long sumP_atSupplyPoint; + static unsigned int samplesDuringDatalogPeriod; + static int RF_send_counter = 0; // counts seconds + + // remove DC offset from the raw voltage sample by subtracting the accurate value + // as determined by a LP filter. + long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long; + + // determine the polarity of the latest voltage sample + enum polarities polarityNow; + if(sampleVminusDC_long > 0) { + polarityNow = POSITIVE; } + else { + polarityNow = NEGATIVE; } + + if (polarityNow == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + cycleCount++; + + triggerNeedsToBeArmed = true; // the trigger is armed once during each +ve half-cycle + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / samplesDuringThisCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / samplesDuringThisCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- for non-standard use + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. + + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) { + realEnergy_diverted = 0; } // to avoid 'creep' + + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + perSecondCounter++; + if(perSecondCounter >= CYCLES_PER_SECOND) + { + perSecondCounter = 0; +/* + Serial.print("Diverted: " ); + Serial.print(divertedEnergyTotal_Wh); + Serial.print(" Wh plus "); + Serial.print((powerCal_diverted / CYCLES_PER_SECOND) * divertedEnergyRecent_IEU); + + Serial.print(" J , EDD is" ); +*/ + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } +/* + if (EDD_isActive) { + Serial.println(" on" ); } + else { + Serial.println(" off" ); } +*/ + configureValueForDisplay(); // occurs every second + + // routine data is to be transmitted every N seconds + RF_send_counter++; + if (RF_send_counter >= RF_SEND_PERIOD) + { + RF_send_counter = 0; + + // calculate the average power at the supply point + long realPower_long = sumP_atSupplyPoint / samplesDuringDatalogPeriod; + tx_data.powerAtSupplyPoint = realPower_long * powerCal_grid; + tx_data.powerAtSupplyPoint *= -1; // To match the OEM convention (so import is +ve) + + sumP_atSupplyPoint = 0; + samplesDuringDatalogPeriod = 0; + +// tx_data.msgNumber = messageNumber; + tx_data.divertedEnergyTotal = divertedEnergyTotal_Wh; + send_rf_data(); +// Serial.print(tx_data.msgNumber); +// Serial.print(", "); + Serial.print(tx_data.powerAtSupplyPoint); + Serial.print(", "); + Serial.println(tx_data.divertedEnergyTotal); +// messageNumber++; + } + + Serial.println (energyInBucket_long * IEUtoJoulesConversion_CT1); + } + + // clear the per-cycle accumulators for use in this new mains cycle. + samplesDuringThisCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + if (triggerNeedsToBeArmed == true) + { + // check to see whether the trigger device can now be reliably armed + if (samplesDuringThisCycle == 3) // much easier than checking the voltage level + { + if (energyInBucket_long < lowerEnergyThreshold_long) { + // when below the lower threshold, always set the triac to "off" + nextStateOfTriac = TRIAC_OFF; } + else + if (energyInBucket_long > upperEnergyThreshold_long) { + // when above the upper threshold, always set the triac to "off" + nextStateOfTriac = TRIAC_ON; } + else { + // otherwise, leave the triac's state unchanged (hysteresis) + } + + // set the Arduino's output pin accordingly, and clear the flag + digitalWrite(outputForTrigger, nextStateOfTriac); + triggerNeedsToBeArmed = false; + + // update the Energy Diversion Detector + if (nextStateOfTriac == TRIAC_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + } + } + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > startUpPeriod * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + samplesDuringThisCycle = 0; + Serial.println ("Go!"); + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + checkOutputModeSelection(); // updates outputMode if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is positive + + // processing for EVERY pair of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform +// long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + sumP_atSupplyPoint +=instP; // cumulative power, for datalogging + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform +// long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + samplesDuringThisCycle++; + samplesDuringDatalogPeriod++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityOfLastSampleV = polarityNow; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + + +// this function changes the value of outputMode if the state of the external switch is altered +void checkOutputModeSelection() +{ + static byte count = 0; + int pinState = digitalRead(outputModeSelectorPin); + if (pinState != outputMode) + { + count++; + } + if (count >= 20) + { + count = 0; + outputMode = (enum outputModes)pinState; // change the global variable + Serial.print ("outputMode selection changed to "); + if (outputMode == NORMAL) { + Serial.println ( "normal"); } + else { + Serial.println ( "anti-flicker"); } + + configureParamsForSelectedOutputMode(); + } +} + + +void configureParamsForSelectedOutputMode() +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + upperEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" lowerEnergyThreshold_long = "); + Serial.println(lowerEnergyThreshold_long); + Serial.print(" upperEnergyThreshold_long = "); + Serial.println(upperEnergyThreshold_long); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + +// Serial.println(divertedEnergyTotal_Wh); + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +// valueToBeDisplayed++; +} + + + + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + +void send_rf_data() +{ + // rf12_sleep(RF12_WAKEUP); + // if ready to send + exit route if it gets stuck + int i = 0; + while (!rf12_canSend() && i<10) + { + rf12_recvDone(); + i++; + } + rf12_sendNow(0, &tx_data, sizeof tx_data); + + // rf12_sendStart(0, &tx_data, sizeof tx_data); + // rf12_sendWait(2); + // rf12_sleep(RF12_SLEEP); +} + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_3.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_3.ino new file mode 100644 index 0000000..666d7cc --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_3.ino @@ -0,0 +1,1074 @@ +/* Mk2_RF_datalog_3.ino + * + * This sketch is for diverting surplus PV power to a dump load using a triac. + * Routine dataloogig is also supported using the on-board RFM12B module. + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The integral voltage sensor is fed from one of the secondary coils of the + * transformer. Current is measured via Current Transformers at the + * CT1 and CT2 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. When the RFM12B module is + * in use, the display can only be used in conjunction with an extra pair of + * logic chips. These are ICs 3 and 4, which reduce the number of processor pins + * that are needed to drive the display. + * + * This sketch is based on the Mk2i PV Router code that I have posted in on the + * OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * September 2014: renamed as Mk2_RFdatalog_3, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - tidier initialisation of display logic in setup(); + * + * Robin Emley + * www.Mk2PVrouter.co.uk + * September 2014 + */ + +#include +#include // JeeLib is available at from: http://github.com/jcw/jeelib +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define SWEETZONE_IN_JOULES 3600 +#define RF_SEND_PERIOD 2 // seconds +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum triacStates {TRIAC_ON, TRIAC_OFF}; // the external trigger device is active low +enum outputModes {ANTI_FLICKER, NORMAL}; + +// ---------------- Extra Features selection ---------------------- +// +// - WORKLOAD_CHECK, for determining how much spare processing time there is. +// +// #define WORKLOAD_CHECK // <-- Include this line is this feature is required + + +// The power-diversion logic can operate in either of two modes: +// +// - NORMAL, where the triac switches rapidly on/off to maintain a constant energy level. +// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations +// of the local mains voltage. +// +// The output mode is determined in realtime via a selector switch +enum outputModes outputMode; + +/* frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ + */ +#define freq RF12_868MHZ // Use the freq to match the module you have. + +const int nodeID = 10; // RFM12B node ID +const int networkGroup = 210; // RFM12B wireless network group - needs to be same as emonBase and emonGLCD +const int UNO = 1; // Set to 0 if you're not using the UNO bootloader (i.e using Duemilanove) + // - All Atmega's shipped from OpenEnergyMonitor come with Arduino Uno bootloader + +int messageNumber = 0; + +// data structure for RF comms +typedef struct { +// int msgNumber; + int powerAtSupplyPoint; // import = +ve, to match OEM convention + int divertedEnergyTotal; // always positive +} Tx_struct; +Tx_struct tx_data; // an instance of this structure type + + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +// D2 is for the RFM12B +const byte outputModeSelectorPin = 3; // <-- with the internal pullup +const byte outputForTrigger = 4; +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +// D10 is for the RFM12B +// D11 is for the RFM12B +// D12 is for the RFM12B +// D13 is for the RFM12B + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + + +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long triggerThreshold_long; // for determining when the trigger may be safely armed +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long lowerEnergyThreshold_long; // for turning triac off +long upperEnergyThreshold_long; // for turning triac on +// int phaseCal_grid_int; // to avoid the need for floating-point maths +// int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; +long mainsCyclesPerHour; + +// this setting is only used if anti-flicker mode is enabled +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.5 + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +int sampleI_grid; +int sampleI_diverted; +int sampleV; + + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// With most hardware, the default values are likely to work fine without +// need for change. A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When +// measuring a resistive load, the voltage and current waveforms should be perfectly +// aligned. In this situation, the Power Factor will be 1. +// +// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct +// value of phaseCal for any hardware configuration. An index of my various Mk2-related +// exhibits is available at http://openenergymonitor.org/emon/node/1757 +// +//const float phaseCal_grid = 1.0; <--- not used in this version +//const float phaseCal_diverted = 1.0; <--- not used in this version + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 10 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. In this version, +// the decimal point has to be treated differently than the other seven segments, so +// a convenient means of accessing this column is provided. +// +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // energy diversion detection +long requiredExportPerMainsCycle_inIEU; +float IEUtoJoulesConversion_CT1; + + +void setup() +{ + pinMode(outputForTrigger, OUTPUT); + digitalWrite (outputForTrigger, TRIAC_OFF); // the external trigger is active low + + pinMode(outputModeSelectorPin, INPUT); + digitalWrite(outputModeSelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(outputModeSelectorPin); // initial selection and + outputMode = (enum outputModes)pinState; // assignment of output mode + + delay(5000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_RFdatalog_3.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, calibration values that have supplied in floating point + // form need to be rescaled. + // +// phaseCal_grid_int = phaseCal_grid * 256; // for integer maths +// phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + configureParamsForSelectedOutputMode(); + + Serial.println ("----"); + +#ifdef WORKLOAD_CHECK + Serial.println ("WELCOME TO WORKLOAD_CHECK "); + +// <<- start of commented out section, to save on RAM space! +/* + Serial.println (" This mode of operation allows the spare processing capacity of the system"); + Serial.println ("to be analysed. Additional delay is gradually increased until all spare time"); + Serial.println ("has been used up. This value (in uS) is noted and the process is repeated. "); + Serial.println ("The delay setting is increased by 1uS at a time, and each value of delay is "); + Serial.println ("checked several times before the delay is increased. "); + */ +// <<- end of commented out section, to save on RAM space! + + Serial.println (" The displayed value is the amount of spare time, per set of V & I samples, "); + Serial.println ("that is available for doing additional processing."); + Serial.println (); + #endif + + rf12_initialize(nodeID, freq, networkGroup); // initialize RF +// rf12_sleep(RF12_SLEEP); <- the RFM12B now stays awake throughout + +} + +// An Interrupt Service Routine is now defined in which the ADC is instructed to +// measure V and I alternately. A "data ready"flag is set after each voltage conversion +// has been completed. +// For each pair of samples, this means that current is measured before voltage. The +// current sample is taken first because the phase of the waveform for current is generally +// slightly advanced relative to the waveform for voltage. The data ready flag is cleared +// within loop(). +// This Interrupt Service Routine is for use when the ADC is fixed timer mode. It is +// executed whenever the ADC timer expires. In this mode, the next ADC conversion is +// initiated from within this ISR. +// +void timerIsr(void) +{ + static unsigned char sample_index = 0; + + switch(sample_index) + { + case 0: + sampleV = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current + ADCSRA |= (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples. The main processor and +// the ADC work autonomously, their operation being only linked via the dataReady flag. +// As soon as a new set of data is made available by the ADC, the main processor can +// start to work on it immediately. +// +void allGeneralProcessing() +{ + static boolean triggerNeedsToBeArmed = false; // once per mains cycle (+ve half) + static int samplesDuringThisCycle; // for normalising the power in each mains cycle + static long sumP_grid; // for per-cycle summation of 'real power' + static long sumP_diverted; // for per-cycle summation of 'real power' + static enum polarities polarityOfLastSampleV; // for zero-crossing detection + static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) + static long lastSampleVminusDC_long; // for the phaseCal algorithm + static byte perSecondCounter = 0; + static enum triacStates nextStateOfTriac = TRIAC_OFF; + + // extra items for datalogging + static long sumP_atSupplyPoint; + static unsigned int samplesDuringDatalogPeriod; + static int RF_send_counter = 0; // counts seconds + + // remove DC offset from the raw voltage sample by subtracting the accurate value + // as determined by a LP filter. + long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long; + + // determine the polarity of the latest voltage sample + enum polarities polarityNow; + if(sampleVminusDC_long > 0) { + polarityNow = POSITIVE; } + else { + polarityNow = NEGATIVE; } + + if (polarityNow == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + + triggerNeedsToBeArmed = true; // the trigger is armed once during each +ve half-cycle + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / samplesDuringThisCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / samplesDuringThisCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- for non-standard use + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. + + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) { + realEnergy_diverted = 0; } // to avoid 'creep' + + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + perSecondCounter++; + if(perSecondCounter >= CYCLES_PER_SECOND) + { + perSecondCounter = 0; +/* + Serial.print("Diverted: " ); + Serial.print(divertedEnergyTotal_Wh); + Serial.print(" Wh plus "); + Serial.print((powerCal_diverted / CYCLES_PER_SECOND) * divertedEnergyRecent_IEU); + + Serial.print(" J , EDD is" ); +*/ + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } +/* + if (EDD_isActive) { + Serial.println(" on" ); } + else { + Serial.println(" off" ); } +*/ + configureValueForDisplay(); // occurs every second + + // routine data is to be transmitted every N seconds + RF_send_counter++; + if (RF_send_counter >= RF_SEND_PERIOD) + { + RF_send_counter = 0; + + // calculate the average power at the supply point + long realPower_long = sumP_atSupplyPoint / samplesDuringDatalogPeriod; + tx_data.powerAtSupplyPoint = realPower_long * powerCal_grid; + tx_data.powerAtSupplyPoint *= -1; // To match the OEM convention (so import is +ve) + + sumP_atSupplyPoint = 0; + samplesDuringDatalogPeriod = 0; + +// tx_data.msgNumber = messageNumber; + tx_data.divertedEnergyTotal = divertedEnergyTotal_Wh; + send_rf_data(); +// Serial.print(tx_data.msgNumber); +// Serial.print(", "); + Serial.print(tx_data.powerAtSupplyPoint); + Serial.print(", "); + Serial.println(tx_data.divertedEnergyTotal); +// messageNumber++; + } + + Serial.println (energyInBucket_long * IEUtoJoulesConversion_CT1); + } + + // clear the per-cycle accumulators for use in this new mains cycle. + samplesDuringThisCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + if (triggerNeedsToBeArmed == true) + { + // check to see whether the trigger device can now be reliably armed + if (samplesDuringThisCycle == 3) // much easier than checking the voltage level + { + if (energyInBucket_long < lowerEnergyThreshold_long) { + // when below the lower threshold, always set the triac to "off" + nextStateOfTriac = TRIAC_OFF; } + else + if (energyInBucket_long > upperEnergyThreshold_long) { + // when above the upper threshold, always set the triac to "off" + nextStateOfTriac = TRIAC_ON; } + else { + // otherwise, leave the triac's state unchanged (hysteresis) + } + + // set the Arduino's output pin accordingly, and clear the flag + digitalWrite(outputForTrigger, nextStateOfTriac); + triggerNeedsToBeArmed = false; + + // update the Energy Diversion Detector + if (nextStateOfTriac == TRIAC_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + } + } + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > startUpPeriod * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + samplesDuringThisCycle = 0; + Serial.println ("Go!"); + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + checkOutputModeSelection(); // updates outputMode if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is positive + + // processing for EVERY pair of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform +// long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + sumP_atSupplyPoint +=instP; // cumulative power, for datalogging + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform +// long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + samplesDuringThisCycle++; + samplesDuringDatalogPeriod++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityOfLastSampleV = polarityNow; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + + +// this function changes the value of outputMode if the state of the external switch is altered +void checkOutputModeSelection() +{ + static byte count = 0; + int pinState = digitalRead(outputModeSelectorPin); + if (pinState != outputMode) + { + count++; + } + if (count >= 20) + { + count = 0; + outputMode = (enum outputModes)pinState; // change the global variable + Serial.print ("outputMode selection changed to "); + if (outputMode == NORMAL) { + Serial.println ( "normal"); } + else { + Serial.println ( "anti-flicker"); } + + configureParamsForSelectedOutputMode(); + } +} + + +void configureParamsForSelectedOutputMode() +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + upperEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" lowerEnergyThreshold_long = "); + Serial.println(lowerEnergyThreshold_long); + Serial.print(" upperEnergyThreshold_long = "); + Serial.println(upperEnergyThreshold_long); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + +// Serial.println(divertedEnergyTotal_Wh); + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +// valueToBeDisplayed++; +} + + + + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + +void send_rf_data() +{ + // rf12_sleep(RF12_WAKEUP); + // if ready to send + exit route if it gets stuck + int i = 0; + while (!rf12_canSend() && i<10) + { + rf12_recvDone(); + i++; + } + rf12_sendNow(0, &tx_data, sizeof tx_data); + + // rf12_sendStart(0, &tx_data, sizeof tx_data); + // rf12_sendWait(2); + // rf12_sleep(RF12_SLEEP); +} + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_4.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_4.ino new file mode 100644 index 0000000..7463248 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_4.ino @@ -0,0 +1,1102 @@ +/* Mk2_RFdatalog_4.ino + * + * This sketch is for diverting surplus PV power to a dump load using a triac. + * Routine dataloogig is also supported using the on-board RFM12B module. + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The integral voltage sensor is fed from one of the secondary coils of the + * transformer. Current is measured via Current Transformers at the + * CT1 and CT2 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. When the RFM12B module is + * in use, the display can only be used in conjunction with an extra pair of + * logic chips. These are ICs 3 and 4, which reduce the number of processor pins + * that are needed to drive the display. + * + * This sketch is based on the Mk2i PV Router code that I have posted in on the + * OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * September 2014: renamed as Mk2_RFdatalog_3, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - tidier initialisation of display logic in setup(); + * + * December 2014, upgraded to Mk2_RFdatalog_4: + * This sketch has been restructured in order to make better use of the ISR. All of + * the time-critical code is now contained within the ISR and its helper functions. + * Values for datalogging are transferred to the main code using a flag-based handshake + * mechanism. The diversion of surplus power can no longer be affected by slower + * activities which may be running in the main code such as Serial statements and RF. + * Temperature sensing is supported by re-allocating the "mode" port for this + * purpose. A pullup resistor (4K7 or similar) is required for the Dallas sensor. The + * output mode, i.e. NORMAL or ANTI_FLICKER, is now set at compile time. + * Also: + * - The ADC is now in free-running mode, at ~104 us per conversion. + * - a persistence check has been added for zero-crossing detection (polarityConfirmed) + * - a lowestNoOfSampleSetsPerMainsCycle check has been added, to detect any disturbances + * - Vrms has been added to the datalog payload (as Vrms x 100) + * - temperature has been added to the datalog payload (as degrees C x 100) + * - the phaseCal mechanism has been reinstated + * + * Robin Emley + * www.Mk2PVrouter.co.uk + * December 2014 + */ + +#include +#include +#include // JeeLib is available at from: http://github.com/jcw/jeelib + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// ----------------------------------------------------- +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define SWEETZONE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// -------------------------- +// Dallas DS18B20 commands +#define SKIP_ROM 0xcc +#define CONVERT_TEMPERATURE 0x44 +#define READ_SCRATCHPAD 0xbe +#define BAD_TEMPERATURE 30000 // this value (300C) is sent if no sensor is present + +// ---------------- +// general literals +#define DATALOG_PERIOD_IN_MAINS_CYCLES 250 +// #define POST_DATALOG_EVENT_DELAY_MILLIS 40 +#define ANTI_CREEP_LIMIT 0 // <- to prevent the diverted energy total from 'creeping' + // in Joules per mains cycle (has no effect when set to 0) + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +// ------------------------------- +// definitions of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum triacStates {TRIAC_ON, TRIAC_OFF}; // the external trigger device is active low +enum outputModes {ANTI_FLICKER, NORMAL}; // retained for compatibility with previous versions. + +// ---- Output mode selection ----- +enum outputModes outputMode = ANTI_FLICKER; // <- needs to be set here unless an +//enum outputModes outputMode = NORMAL; // external switch is in use + +/* -------------------------------------- + * RF configuration (for the RFM12B module) + * frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ + */ +#define freq RF12_868MHZ + +const int nodeID = 10; // RFM12B node ID +const int networkGroup = 210; // wireless network group - needs to be same for all nodes +const int UNO = 1; // for when the processor contains the UNO bootloader. + +typedef struct { + int powerAtSupplyPoint_Watts; // import = +ve, to match OEM convention + int divertedEnergyTotal_Wh; // always positive + int Vrms_times100; + int temperature_times100; +} Tx_struct; +Tx_struct tx_data; + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +// D2 is for the RFM12B +const byte tempSensorPin = 3; // <-- the "mode" port +const byte outputForTrigger = 4; +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +// D10 is for the RFM12B +// D11 is for the RFM12B +// D12 is for the RFM12B +// D13 is for the RFM12B + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + +const byte delayBeforeSerialStarts = 5; // in seconds, to allow Serial window to be opened +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +/* ------------------------------------------------------------------------------------- + * Global variables that are used in multiple blocks so cannot be static. + * For integer maths, many variables need to be 'long' + */ +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long energyInBucket_long; // in Integer Energy Units (for controlling the dump-load) +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long lowerEnergyThreshold_long; // for turning load off +long upperEnergyThreshold_long; // for turning load on +int phaseCal_grid_int; // to avoid the need for floating-point maths +int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF + +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; +float IEUtoJoulesConversion_CT1; + +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- not wise to exceeed 0.4 + +long sumP_forEnergyBucket; // for per-cycle summation of 'real power' +long sumP_diverted; // for per-cycle summation of diverted power +long sumP_atSupplyPoint; // for summation of 'real power' values during datalog period +long sum_Vsquared; // for summation of V^2 values during datalog period +int sampleSetsDuringThisCycle; // for counting the sample sets during each mains cycle +long sampleSetsDuringThisDatalogPeriod; // for counting the sample sets during each datalogging period + +long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) +long lastSampleVminusDC_long; // for the phaseCal algorithm +byte cycleCountForDatalogging = 0; +long sampleVminusDC_long; +long requiredExportPerMainsCycle_inIEU; + +// for interaction between the main code and the ISR +volatile boolean datalogEventPending = false; +volatile boolean newMainsCycle = false; + +long copyOf_sumP_atSupplyPoint; +long copyOf_sum_Vsquared; +long copyOf_divertedEnergyTotal_Wh; +int copyOf_lowestNoOfSampleSetsPerMainsCycle; +long copyOf_sampleSetsDuringThisDatalogPeriod; + +// For temperature sensing +OneWire oneWire(tempSensorPin); +int tempTimes100; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define POLARITY_CHECK_MAXCOUNT 2 +enum polarities polarityNow; +enum polarities polarityConfirmed; // for zero-crossing detection +enum polarities polarityConfirmedOfLastSampleV; // for zero-crossing detection + +// For a mechanism to check the integrity of this code structure +int lowestNoOfSampleSetsPerMainsCycle; +unsigned long timeAtLastDelay; + + +// Calibration values (not important for the Router's basic operation) +//------------------- +// For accurate calculation of real power/energy, two calibration values are +// used: powerCal and phaseCal. With most hardware, the default values are +// likely to work fine without need for change. A full explanation of each +// of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool_2chan.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. Any pre-built system that I supply will have been +// checked with this tool to ensure that the input sensors are working correctly. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 230V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 230V AC has a peak-to-peak amplitude of 651V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +// for 3.3V operation, the optimum value is generally around 0.044 +// for 5V operation, the optimum value is generally around 0.072 +// +const float powerCal_grid = 0.072; +const float powerCal_diverted = 0.073; + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. This mechanism can be used to offset any difference in +// phase delay between the voltage and current sensors. The algorithm interpolates +// between the most recent pair of voltage samples according to the phaseCal value. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value is used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. +// +// The calculation for real power is very insensitive to the value of phaseCal. +// When a "real power" calculation is used to determine how much surplus energy +// is available for diversion, a nominal value such as 1.0 is generally thought +// to be sufficient for this purpose. +// +const float phaseCal_grid = 1.0; +const float phaseCal_diverted = 1.0; + + +// For datalogging purposes, voltageCal has been included too. When running at +// 230 V AC, the range of ADC values will be similar to the actual range of volts, +// so the optimal value for this cal factor will be close to unity. +// +const float voltageCal = 1.0; + + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 10 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of this array is for the decimal point status. +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +volatile boolean EDD_isActive = false; // energy diversion detection +//volatile boolean EDD_isActive = true; // energy diversion detection + + +void setup() +{ + pinMode(outputForTrigger, OUTPUT); + digitalWrite (outputForTrigger, TRIAC_OFF); // the external trigger is active low + + delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_RFdatalog_4.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, calibration values that are supplied in floating point + // form need to be rescaled. + // + phaseCal_grid_int = phaseCal_grid * 256; // for integer maths + phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail just before the energy bucket is updated at the start + // of each new mains cycle. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone's value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1): + capacityOfEnergyBucket_long = + (long)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up + + IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; // may be useful + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled in a known way. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_diverted); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.println ("ADC mode: free-running"); + + // Set up the ADC to be free-running + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + configureParamsForSelectedOutputMode(); + Serial.println ("----"); + + convertTemperature(); // start initial temperature conversion + + rf12_initialize(nodeID, freq, networkGroup); // initialize RF +// rf12_sleep(RF12_SLEEP); <- the RFM12B now stays awake throughout + + convertTemperature(); // start initial temperature conversion +} + +/* None of the workload in loop() is time-critical. All the processing of + * ADC data is done within the ISR. + */ +void loop() +{ +// unsigned long timeNow = millis(); + static byte perSecondTimer = 0; +// + // The ISR provides a 50 Hz 'tick' which the main code is free to use. + if (newMainsCycle) + { + newMainsCycle = false; + perSecondTimer++; + + if(perSecondTimer >= CYCLES_PER_SECOND) + { + perSecondTimer = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // Clear the accumulators for diverted energy. These are the "genuine" + // accumulators that are used by ISR rather than the copies that are + // regularly made available for use by the main code. + // + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); // this timing is not critical so does not need to be in the ISR + } + } + + if (datalogEventPending) + { + datalogEventPending= false; + tx_data.powerAtSupplyPoint_Watts = copyOf_sumP_atSupplyPoint * powerCal_grid / copyOf_sampleSetsDuringThisDatalogPeriod; + tx_data.divertedEnergyTotal_Wh = copyOf_divertedEnergyTotal_Wh; + tx_data.Vrms_times100 = (int)(100 * voltageCal * sqrt(copyOf_sum_Vsquared / copyOf_sampleSetsDuringThisDatalogPeriod)); + tx_data.temperature_times100 = readTemperature(); + send_rf_data(); + + Serial.print("datalog event: grid power "); Serial.print(tx_data.powerAtSupplyPoint_Watts); + Serial.print(", diverted energy (Wh) "); Serial.print(tx_data.divertedEnergyTotal_Wh); + Serial.print(", Vrms "); Serial.print((float)tx_data.Vrms_times100 / 100); + Serial.print(", temperature "); Serial.print((float)tx_data.temperature_times100 / 100); + Serial.print(", (minSampleSets/MC "); Serial.print(copyOf_lowestNoOfSampleSetsPerMainsCycle); + Serial.print(", #ofSampleSets "); Serial.print(copyOf_sampleSetsDuringThisDatalogPeriod); + Serial.println(')'); +// delay(POST_DATALOG_EVENT_DELAY_MILLIS); + convertTemperature(); // for use next time around + } + +/* + // occasional delays should not affect the operation of this revised code structure. + if (timeNow - timeAtLastDelay > 1000) + { + delay(100); + Serial.println("100ms delay"); + timeAtLastDelay = timeNow; + } +*/ +} + + +ISR(ADC_vect) +/* + * This Interrupt Service Routine looks after the acquisition and processing of + * raw samples from the ADC sub-processor. By means of various helper functions, all of + * the time-critical activities are processed within the ISR. The main code is notified + * by means of a flag when fresh copies of loggable data are available. + */ +{ + static unsigned char sample_index = 0; + int rawSample; + long sampleIminusDC; + long phaseShiftedSampleVminusDC; + long filtV_div4; + long filtI_div4; + long instP; + long inst_Vsquared; + + switch(sample_index) + { + case 0: + rawSample = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // the conversion for I_grid is already under way + sample_index++; // increment the control flag + // + sampleVminusDC_long = ((long)rawSample<<8) - DCoffset_V_long; + if(sampleVminusDC_long > 0) { + polarityNow = POSITIVE; } + else { + polarityNow = NEGATIVE; } + confirmPolarity(); + // + checkProgress(); // deals with aspects that only occur at particular stages of each mains cycle + // + // for the Vrms calculation (for datalogging only) + filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (now x4096, or 2^12) + inst_Vsquared = inst_Vsquared>>12; // scaling is now x1 (V_ADC x I_ADC) + sum_Vsquared += inst_Vsquared; // cumulative V^2 (V_ADC x I_ADC) + sampleSetsDuringThisDatalogPeriod++; + // + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + sampleSetsDuringThisCycle++; // for real power calculations + refreshDisplay(); + break; + case 1: + rawSample = ADC; // store the ADC value (this one is for Grid Current) + ADMUX = 0x40 + voltageSensor; // the conversion for I_diverted is already under way + sample_index++; // increment the control flag + // + // remove most of the DC offset from the current sample (the precise value does not matter) + sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8; + // + // phase-shift the voltage waveform so that it aligns with the grid current waveform + phaseShiftedSampleVminusDC = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + // + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + + sumP_forEnergyBucket+=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + sumP_atSupplyPoint +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + break; + case 2: + rawSample = ADC; // store the ADC value (this one is for Diverted Current) + ADMUX = 0x40 + currentSensor_grid; // the conversion for Voltage is already under way + sample_index = 0; // reset the control flag + // + // remove most of the DC offset from the current sample (the precise value does not matter) + sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8; + // + // phase-shift the voltage waveform so that it aligns with the diverted current waveform + phaseShiftedSampleVminusDC = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + // + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + break; + default: + sample_index = 0; // to prevent lockup (should never get here) + } +} + +/* ----------------------------------------------------------- + * Start of various helper functions which are used by the ISR + */ + +void checkProgress() +/* + * This routine is called by the ISR when each voltage sample becomes available. + * At the start of each new mains cycle, another helper function is called. + * All other processing is done within this function. + */ +{ + static enum triacStates nextStateOfTriac = TRIAC_OFF; + + if (polarityConfirmed == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + // The start of a new mains cycle, just after the +ve going zero-crossing point. + + // a simple routine for checking the performance of this new ISR structure + if (sampleSetsDuringThisCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisCycle; } + + processLatestContribution(); // for activities at the start of each new mains cycle + + } // end of processing that is specific to the first +ve Vsample in each mains cycle + + // still processing samples where the voltage is POSITIVE ... + // check to see whether the trigger device can now be reliably armed + // + if (sampleSetsDuringThisCycle == 5) // part way through the +ve half cycle + { + if (energyInBucket_long < lowerEnergyThreshold_long) { + // when below the lower threshold, always set the triac to "off" + nextStateOfTriac = TRIAC_OFF; } + else + if (energyInBucket_long > upperEnergyThreshold_long) { + // when above the upper threshold, always set the triac to "off" + nextStateOfTriac = TRIAC_ON; } + else { + } // leave the triac's state unchanged (hysteresis) + + // set the Arduino's output pin accordingly + digitalWrite(outputForTrigger, nextStateOfTriac); + + // update the Energy Diversion Detector + if (nextStateOfTriac == TRIAC_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + } + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000) + { + beyondStartUpPhase = true; + sumP_forEnergyBucket = 0; + sumP_atSupplyPoint; + sumP_diverted = 0; + sampleSetsDuringThisCycle = 0; + sampleSetsDuringThisDatalogPeriod = 0; + } + } + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // Half way through the mains cycle, just after the -ve going zero-crossing point. + // This is a convenient place to update the Low Pass Filter for DC-offset removal. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 230V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need for a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + +// checkOutputModeSelection(); // updates outputMode if the external switch is in use + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is negative +} // end of checkProgress() + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. It forms part of the ISR. + */ + static byte count = 0; + if (polarityNow != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count >= POLARITY_CHECK_MAXCOUNT) + { + count = 0; + polarityConfirmed = polarityNow; + } +} + + +void processLatestContribution() +/* + * This routine runs once per mains cycle. It forms part of the ISR. + */ +{ + newMainsCycle = true; // <-- a 50 Hz 'tick' for use by the main code + + // For the mechanism which controls the diversion of surplus power, the AVERAGE power + // at the 'grid' point during the previous mains cycle must be quantified. The first + // stage in this process is for the sum of all instantaneous power values to be divided + // by the number of sample sets that have contributed to its value. A similar operation + // is required for the diverted power data. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_for_energyBucket = sumP_forEnergyBucket / sampleSetsDuringThisCycle; + long realPower_diverted = sumP_diverted / sampleSetsDuringThisCycle; + // + // The per-mainsCycle variables can now be reset for ongoing use + sampleSetsDuringThisCycle = 0; + sumP_forEnergyBucket = 0; + sumP_diverted = 0; + + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Average power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variables below are CYCLES_PER_SECOND * (1/powerCal) times larger than + // their actual values in Joules. + // + long realEnergy_for_energyBucket = realPower_for_energyBucket; + long realEnergy_diverted = realPower_diverted; + + // The latest energy contribution from the grid connection point can now be added + // to the energy bucket which determines the state of the dump-load. + // + energyInBucket_long += realEnergy_for_energyBucket; + energyInBucket_long -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation + + // Apply max and min limits to the bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. To avoid the displayed + // value from creeping, any small contributions which are likely to be + // caused by noise are ignored. + // + if (realEnergy_diverted > antiCreepLimit_inIEUperMainsCycle) { + divertedEnergyRecent_IEU += realEnergy_diverted; } + + // Whole Watt-Hours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + /* At the end of each datalogging period, copies are made of the relevant variables + * for use by the main code. These variable are then reset for use during the next + * datalogging period. + */ + cycleCountForDatalogging ++; + if (cycleCountForDatalogging >= DATALOG_PERIOD_IN_MAINS_CYCLES ) + { + cycleCountForDatalogging = 0; + + copyOf_sumP_atSupplyPoint = sumP_atSupplyPoint; + copyOf_divertedEnergyTotal_Wh = divertedEnergyTotal_Wh; + copyOf_sum_Vsquared = sum_Vsquared; + copyOf_sampleSetsDuringThisDatalogPeriod = sampleSetsDuringThisDatalogPeriod; // (for diags only) + copyOf_lowestNoOfSampleSetsPerMainsCycle = lowestNoOfSampleSetsPerMainsCycle; // (for diags only) + + sumP_atSupplyPoint = 0; + sum_Vsquared = 0; + lowestNoOfSampleSetsPerMainsCycle = 999; + sampleSetsDuringThisDatalogPeriod = 0; + datalogEventPending = true; + } +} +/* End of helper functions which are used by the ISR + * ------------------------------------------------- + */ + +// this function changes the value of outputMode if the external switch is in use for this purpose +void checkOutputModeSelection() +{ + static byte count = 0; + int pinState; + // pinState = digitalRead(outputModeSelectorPin); <- pin re-allocated for Dallas sensor + if (pinState != outputMode) + { + count++; + } + if (count >= 20) + { + count = 0; + outputMode = (enum outputModes)pinState; // change the global variable + Serial.print ("outputMode selection changed to "); + if (outputMode == NORMAL) { + Serial.println ( "normal"); } + else { + Serial.println ( "anti-flicker"); } + + configureParamsForSelectedOutputMode(); + } +} + + + +void configureParamsForSelectedOutputMode() +/* + * retained for compatibility with previous versions + */ +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + upperEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" lowerEnergyThreshold_long = "); + Serial.println(lowerEnergyThreshold_long); + Serial.print(" upperEnergyThreshold_long = "); + Serial.println(upperEnergyThreshold_long); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +} + + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + +void convertTemperature() +{ + oneWire.reset(); + oneWire.write(SKIP_ROM); + oneWire.write(CONVERT_TEMPERATURE); +} + +int readTemperature() +{ + byte buf[9]; + int result; + + oneWire.reset(); + oneWire.write(SKIP_ROM); + oneWire.write(READ_SCRATCHPAD); + for(int i=0; i<9; i++) buf[i]=oneWire.read(); + if(oneWire.crc8(buf,8)==buf[8]) + { + result=(buf[1]<<8)|buf[0]; + // result is temperature x16, multiply by 6.25 to convert to temperature x100 + result=(result*6)+(result>>2); + } + else result=BAD_TEMPERATURE; + return result; +} + + +void send_rf_data() +// +// To avoid disturbance to the sampling process, the RFM12B needs to remain in its +// active state rather than being periodically put to sleep. +{ + // check whether it's ready to send, and an exit route if it gets stuck + int i = 0; + while (!rf12_canSend() && i<10) + { + rf12_recvDone(); + i++; + } + rf12_sendNow(0, &tx_data, sizeof tx_data); +} + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_4a.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_4a.ino new file mode 100644 index 0000000..6e01d6d --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_4a.ino @@ -0,0 +1,1114 @@ +/* Mk2_RFdatalog_4a.ino + * + * This sketch is for diverting surplus PV power to a dump load using a triac. + * Routine datalogging is also supported using the on-board RFM12B module. + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The integral voltage sensor is fed from one of the secondary coils of the + * transformer. Current is measured via Current Transformers at the + * CT1 and CT2 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. When the RFM12B module is + * in use, the display can only be used in conjunction with an extra pair of + * logic chips. These are ICs 3 and 4, which reduce the number of processor pins + * that are needed to drive the display. + * + * This sketch is based on the Mk2i PV Router code that I have posted in on the + * OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * September 2014: renamed as Mk2_RFdatalog_3, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - tidier initialisation of display logic in setup(); + * + * December 2014, upgraded to Mk2_RFdatalog_4: + * This sketch has been restructured in order to make better use of the ISR. All of + * the time-critical code is now contained within the ISR and its helper functions. + * Values for datalogging are transferred to the main code using a flag-based handshake + * mechanism. The diversion of surplus power can no longer be affected by slower + * activities which may be running in the main code such as Serial statements and RF. + * Temperature sensing is supported by re-allocating the "mode" port for this + * purpose. A pullup resistor (4K7 or similar) is required for the Dallas sensor. The + * output mode, i.e. NORMAL or ANTI_FLICKER, is now set at compile time. + * Also: + * - The ADC is now in free-running mode, at ~104 us per conversion. + * - a persistence check has been added for zero-crossing detection (polarityConfirmed) + * - a lowestNoOfSampleSetsPerMainsCycle check has been added, to detect any disturbances + * - Vrms has been added to the datalog payload (as Vrms x 100) + * - temperature has been added to the datalog payload (as degrees C x 100) + * - the phaseCal mechanism has been reinstated + * + * January 2016: renamed as Mk2_RFdatalog_4a, with a minor change in the ISR to reinstate + * the phaseCal calculation. Previously, this feature was having no effect because + * two assignment lines were in the wrong order. When measuring "real power", which + * is what this application does, the phaseCal refinement has very little effect even + * when correctly implemented, as it now is. + * Support for the RF69 RF module has also been added. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#define RF69_COMPAT 0 // <-- include this line for the RFM12B +// #define RF69_COMPAT 1 // <-- include this line for the RF69 + + +#include +#include +#include // JeeLib is available at from: http://github.com/jcw/jeelib + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// ----------------------------------------------------- +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define SWEETZONE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// -------------------------- +// Dallas DS18B20 commands +#define SKIP_ROM 0xcc +#define CONVERT_TEMPERATURE 0x44 +#define READ_SCRATCHPAD 0xbe +#define BAD_TEMPERATURE 30000 // this value (300C) is sent if no sensor is present + +// ---------------- +// general literals +#define DATALOG_PERIOD_IN_MAINS_CYCLES 250 +// #define POST_DATALOG_EVENT_DELAY_MILLIS 40 +#define ANTI_CREEP_LIMIT 0 // <- to prevent the diverted energy total from 'creeping' + // in Joules per mains cycle (has no effect when set to 0) + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +// ------------------------------- +// definitions of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum triacStates {TRIAC_ON, TRIAC_OFF}; // the external trigger device is active low +enum outputModes {ANTI_FLICKER, NORMAL}; // retained for compatibility with previous versions. + +// ---- Output mode selection ----- +enum outputModes outputMode = ANTI_FLICKER; // <- needs to be set here unless an +//enum outputModes outputMode = NORMAL; // external switch is in use + +/* -------------------------------------- + * RF configuration (for the RFM12B module) + * frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ + */ +#define freq RF12_433MHZ + +const int nodeID = 10; // RFM12B node ID +const int networkGroup = 210; // wireless network group - needs to be same for all nodes +const int UNO = 1; // for when the processor contains the UNO bootloader. + +typedef struct { + int powerAtSupplyPoint_Watts; // import = +ve, to match OEM convention + int divertedEnergyTotal_Wh; // always positive + int Vrms_times100; + int temperature_times100; +} Tx_struct; +Tx_struct tx_data; + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +// D2 is for the RFM12B +const byte tempSensorPin = 3; // <-- the "mode" port +const byte outputForTrigger = 4; +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +// D10 is for the RFM12B +// D11 is for the RFM12B +// D12 is for the RFM12B +// D13 is for the RFM12B + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + +const byte delayBeforeSerialStarts = 5; // in seconds, to allow Serial window to be opened +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +/* ------------------------------------------------------------------------------------- + * Global variables that are used in multiple blocks so cannot be static. + * For integer maths, many variables need to be 'long' + */ +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long energyInBucket_long; // in Integer Energy Units (for controlling the dump-load) +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long lowerEnergyThreshold_long; // for turning load off +long upperEnergyThreshold_long; // for turning load on +int phaseCal_grid_int; // to avoid the need for floating-point maths +int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF + +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; +float IEUtoJoulesConversion_CT1; + +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- not wise to exceeed 0.4 + +long sumP_forEnergyBucket; // for per-cycle summation of 'real power' +long sumP_diverted; // for per-cycle summation of diverted power +long sumP_atSupplyPoint; // for summation of 'real power' values during datalog period +long sum_Vsquared; // for summation of V^2 values during datalog period +int sampleSetsDuringThisCycle; // for counting the sample sets during each mains cycle +long sampleSetsDuringThisDatalogPeriod; // for counting the sample sets during each datalogging period + +long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) +long lastSampleVminusDC_long; // for the phaseCal algorithm +byte cycleCountForDatalogging = 0; +long sampleVminusDC_long; +long requiredExportPerMainsCycle_inIEU; + +// for interaction between the main code and the ISR +volatile boolean datalogEventPending = false; +volatile boolean newMainsCycle = false; + +long copyOf_sumP_atSupplyPoint; +long copyOf_sum_Vsquared; +long copyOf_divertedEnergyTotal_Wh; +int copyOf_lowestNoOfSampleSetsPerMainsCycle; +long copyOf_sampleSetsDuringThisDatalogPeriod; + +// For temperature sensing +OneWire oneWire(tempSensorPin); +int tempTimes100; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define POLARITY_CHECK_MAXCOUNT 2 +enum polarities polarityNow; +enum polarities polarityConfirmed; // for zero-crossing detection +enum polarities polarityConfirmedOfLastSampleV; // for zero-crossing detection + +// For a mechanism to check the integrity of this code structure +int lowestNoOfSampleSetsPerMainsCycle; +unsigned long timeAtLastDelay; + + +// Calibration values (not important for the Router's basic operation) +//------------------- +// For accurate calculation of real power/energy, two calibration values are +// used: powerCal and phaseCal. With most hardware, the default values are +// likely to work fine without need for change. A full explanation of each +// of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool_2chan.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. Any pre-built system that I supply will have been +// checked with this tool to ensure that the input sensors are working correctly. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 230V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 230V AC has a peak-to-peak amplitude of 651V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +// for 3.3V operation, the optimum value is generally around 0.044 +// for 5V operation, the optimum value is generally around 0.072 +// +const float powerCal_grid = 0.072; +const float powerCal_diverted = 0.073; + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. This mechanism can be used to offset any difference in +// phase delay between the voltage and current sensors. The algorithm interpolates +// between the most recent pair of voltage samples according to the phaseCal value. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value is used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. +// +// The calculation for real power is very insensitive to the value of phaseCal. +// When a "real power" calculation is used to determine how much surplus energy +// is available for diversion, a nominal value such as 1.0 is generally thought +// to be sufficient for this purpose. +// +const float phaseCal_grid = 1.0; +const float phaseCal_diverted = 1.0; + + +// For datalogging purposes, voltageCal has been included too. When running at +// 230 V AC, the range of ADC values will be similar to the actual range of volts, +// so the optimal value for this cal factor will be close to unity. +// +const float voltageCal = 1.0; + + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of this array is for the decimal point status. +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +volatile boolean EDD_isActive = false; // energy diversion detection +//volatile boolean EDD_isActive = true; // energy diversion detection + + +void setup() +{ + pinMode(outputForTrigger, OUTPUT); + digitalWrite (outputForTrigger, TRIAC_OFF); // the external trigger is active low + + delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_RFdatalog_4a.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, calibration values that are supplied in floating point + // form need to be rescaled. + // + phaseCal_grid_int = phaseCal_grid * 256; // for integer maths + phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail just before the energy bucket is updated at the start + // of each new mains cycle. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone's value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1): + capacityOfEnergyBucket_long = + (long)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up + + IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; // may be useful + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled in a known way. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_diverted); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.println ("ADC mode: free-running"); + + // Set up the ADC to be free-running + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + configureParamsForSelectedOutputMode(); + Serial.println ("----"); + + convertTemperature(); // start initial temperature conversion + + rf12_initialize(nodeID, freq, networkGroup); // initialize RF +// rf12_sleep(RF12_SLEEP); <- the RFM12B now stays awake throughout + + convertTemperature(); // start initial temperature conversion +} + +/* None of the workload in loop() is time-critical. All the processing of + * ADC data is done within the ISR. + */ +void loop() +{ +// unsigned long timeNow = millis(); + static byte perSecondTimer = 0; +// + // The ISR provides a 50 Hz 'tick' which the main code is free to use. + if (newMainsCycle) + { + newMainsCycle = false; + perSecondTimer++; + + if(perSecondTimer >= CYCLES_PER_SECOND) + { + perSecondTimer = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // Clear the accumulators for diverted energy. These are the "genuine" + // accumulators that are used by ISR rather than the copies that are + // regularly made available for use by the main code. + // + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); // this timing is not critical so does not need to be in the ISR + } + } + + if (datalogEventPending) + { + datalogEventPending= false; + tx_data.powerAtSupplyPoint_Watts = copyOf_sumP_atSupplyPoint * powerCal_grid / copyOf_sampleSetsDuringThisDatalogPeriod; + tx_data.powerAtSupplyPoint_Watts *= -1; // to match the OEM convention (import is =ve; export is -ve) + tx_data.divertedEnergyTotal_Wh = copyOf_divertedEnergyTotal_Wh; + tx_data.Vrms_times100 = (int)(100 * voltageCal * sqrt(copyOf_sum_Vsquared / copyOf_sampleSetsDuringThisDatalogPeriod)); + tx_data.temperature_times100 = readTemperature(); + send_rf_data(); + + Serial.print("datalog event: grid power "); Serial.print(tx_data.powerAtSupplyPoint_Watts); + Serial.print(", diverted energy (Wh) "); Serial.print(tx_data.divertedEnergyTotal_Wh); + Serial.print(", Vrms "); Serial.print((float)tx_data.Vrms_times100 / 100); + Serial.print(", temperature "); Serial.print((float)tx_data.temperature_times100 / 100); + Serial.print(", (minSampleSets/MC "); Serial.print(copyOf_lowestNoOfSampleSetsPerMainsCycle); + Serial.print(", #ofSampleSets "); Serial.print(copyOf_sampleSetsDuringThisDatalogPeriod); + Serial.println(')'); +// delay(POST_DATALOG_EVENT_DELAY_MILLIS); + convertTemperature(); // for use next time around + } + +/* + // occasional delays should not affect the operation of this revised code structure. + if (timeNow - timeAtLastDelay > 1000) + { + delay(100); + Serial.println("100ms delay"); + timeAtLastDelay = timeNow; + } +*/ +} + + +ISR(ADC_vect) +/* + * This Interrupt Service Routine looks after the acquisition and processing of + * raw samples from the ADC sub-processor. By means of various helper functions, all of + * the time-critical activities are processed within the ISR. The main code is notified + * by means of a flag when fresh copies of loggable data are available. + */ +{ + static unsigned char sample_index = 0; + int rawSample; + long sampleIminusDC; + long phaseShiftedSampleVminusDC; + long filtV_div4; + long filtI_div4; + long instP; + long inst_Vsquared; + + switch(sample_index) + { + case 0: + rawSample = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // the conversion for I_grid is already under way + sample_index++; // increment the control flag + // + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + sampleVminusDC_long = ((long)rawSample<<8) - DCoffset_V_long; + if(sampleVminusDC_long > 0) { + polarityNow = POSITIVE; } + else { + polarityNow = NEGATIVE; } + confirmPolarity(); + // + checkProgress(); // deals with aspects that only occur at particular stages of each mains cycle + // + // for the Vrms calculation (for datalogging only) + filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (now x4096, or 2^12) + inst_Vsquared = inst_Vsquared>>12; // scaling is now x1 (V_ADC x I_ADC) + sum_Vsquared += inst_Vsquared; // cumulative V^2 (V_ADC x I_ADC) + sampleSetsDuringThisDatalogPeriod++; + // + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter +// lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + sampleSetsDuringThisCycle++; // for real power calculations + refreshDisplay(); + break; + case 1: + rawSample = ADC; // store the ADC value (this one is for Grid Current) + ADMUX = 0x40 + voltageSensor; // the conversion for I_diverted is already under way + sample_index++; // increment the control flag + // + // remove most of the DC offset from the current sample (the precise value does not matter) + sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8; + // + // phase-shift the voltage waveform so that it aligns with the grid current waveform + phaseShiftedSampleVminusDC = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + // + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + + sumP_forEnergyBucket+=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + sumP_atSupplyPoint +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + break; + case 2: + rawSample = ADC; // store the ADC value (this one is for Diverted Current) + ADMUX = 0x40 + currentSensor_grid; // the conversion for Voltage is already under way + sample_index = 0; // reset the control flag + // + // remove most of the DC offset from the current sample (the precise value does not matter) + sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8; + // + // phase-shift the voltage waveform so that it aligns with the diverted current waveform + phaseShiftedSampleVminusDC = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + // + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + break; + default: + sample_index = 0; // to prevent lockup (should never get here) + } +} + +/* ----------------------------------------------------------- + * Start of various helper functions which are used by the ISR + */ + +void checkProgress() +/* + * This routine is called by the ISR when each voltage sample becomes available. + * At the start of each new mains cycle, another helper function is called. + * All other processing is done within this function. + */ +{ + static enum triacStates nextStateOfTriac = TRIAC_OFF; + + if (polarityConfirmed == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + // The start of a new mains cycle, just after the +ve going zero-crossing point. + + // a simple routine for checking the performance of this new ISR structure + if (sampleSetsDuringThisCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisCycle; } + + processLatestContribution(); // for activities at the start of each new mains cycle + + } // end of processing that is specific to the first +ve Vsample in each mains cycle + + // still processing samples where the voltage is POSITIVE ... + // check to see whether the trigger device can now be reliably armed + // + if (sampleSetsDuringThisCycle == 5) // part way through the +ve half cycle + { + if (energyInBucket_long < lowerEnergyThreshold_long) { + // when below the lower threshold, always set the triac to "off" + nextStateOfTriac = TRIAC_OFF; } + else + if (energyInBucket_long > upperEnergyThreshold_long) { + // when above the upper threshold, always set the triac to "off" + nextStateOfTriac = TRIAC_ON; } + else { + } // leave the triac's state unchanged (hysteresis) + + // set the Arduino's output pin accordingly + digitalWrite(outputForTrigger, nextStateOfTriac); + + // update the Energy Diversion Detector + if (nextStateOfTriac == TRIAC_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + } + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000) + { + beyondStartUpPhase = true; + sumP_forEnergyBucket = 0; + sumP_atSupplyPoint; + sumP_diverted = 0; + sampleSetsDuringThisCycle = 0; + sampleSetsDuringThisDatalogPeriod = 0; + } + } + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // Half way through the mains cycle, just after the -ve going zero-crossing point. + // This is a convenient place to update the Low Pass Filter for DC-offset removal. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 230V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need for a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + +// checkOutputModeSelection(); // updates outputMode if the external switch is in use + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is negative +} // end of checkProgress() + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. It forms part of the ISR. + */ + static byte count = 0; + if (polarityNow != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count >= POLARITY_CHECK_MAXCOUNT) + { + count = 0; + polarityConfirmed = polarityNow; + } +} + + +void processLatestContribution() +/* + * This routine runs once per mains cycle. It forms part of the ISR. + */ +{ + newMainsCycle = true; // <-- a 50 Hz 'tick' for use by the main code + + // For the mechanism which controls the diversion of surplus power, the AVERAGE power + // at the 'grid' point during the previous mains cycle must be quantified. The first + // stage in this process is for the sum of all instantaneous power values to be divided + // by the number of sample sets that have contributed to its value. A similar operation + // is required for the diverted power data. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_for_energyBucket = sumP_forEnergyBucket / sampleSetsDuringThisCycle; + long realPower_diverted = sumP_diverted / sampleSetsDuringThisCycle; + // + // The per-mainsCycle variables can now be reset for ongoing use + sampleSetsDuringThisCycle = 0; + sumP_forEnergyBucket = 0; + sumP_diverted = 0; + + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Average power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variables below are CYCLES_PER_SECOND * (1/powerCal) times larger than + // their actual values in Joules. + // + long realEnergy_for_energyBucket = realPower_for_energyBucket; + long realEnergy_diverted = realPower_diverted; + + // The latest energy contribution from the grid connection point can now be added + // to the energy bucket which determines the state of the dump-load. + // + energyInBucket_long += realEnergy_for_energyBucket; + energyInBucket_long -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation + + // Apply max and min limits to the bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. To avoid the displayed + // value from creeping, any small contributions which are likely to be + // caused by noise are ignored. + // + if (realEnergy_diverted > antiCreepLimit_inIEUperMainsCycle) { + divertedEnergyRecent_IEU += realEnergy_diverted; } + + // Whole Watt-Hours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + /* At the end of each datalogging period, copies are made of the relevant variables + * for use by the main code. These variable are then reset for use during the next + * datalogging period. + */ + cycleCountForDatalogging ++; + if (cycleCountForDatalogging >= DATALOG_PERIOD_IN_MAINS_CYCLES ) + { + cycleCountForDatalogging = 0; + + copyOf_sumP_atSupplyPoint = sumP_atSupplyPoint; + copyOf_divertedEnergyTotal_Wh = divertedEnergyTotal_Wh; + copyOf_sum_Vsquared = sum_Vsquared; + copyOf_sampleSetsDuringThisDatalogPeriod = sampleSetsDuringThisDatalogPeriod; // (for diags only) + copyOf_lowestNoOfSampleSetsPerMainsCycle = lowestNoOfSampleSetsPerMainsCycle; // (for diags only) + + sumP_atSupplyPoint = 0; + sum_Vsquared = 0; + lowestNoOfSampleSetsPerMainsCycle = 999; + sampleSetsDuringThisDatalogPeriod = 0; + datalogEventPending = true; + } +} +/* End of helper functions which are used by the ISR + * ------------------------------------------------- + */ + +// this function changes the value of outputMode if the external switch is in use for this purpose +void checkOutputModeSelection() +{ + static byte count = 0; + int pinState; + // pinState = digitalRead(outputModeSelectorPin); <- pin re-allocated for Dallas sensor + if (pinState != outputMode) + { + count++; + } + if (count >= 20) + { + count = 0; + outputMode = (enum outputModes)pinState; // change the global variable + Serial.print ("outputMode selection changed to "); + if (outputMode == NORMAL) { + Serial.println ( "normal"); } + else { + Serial.println ( "anti-flicker"); } + + configureParamsForSelectedOutputMode(); + } +} + + + +void configureParamsForSelectedOutputMode() +/* + * retained for compatibility with previous versions + */ +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + upperEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" lowerEnergyThreshold_long = "); + Serial.println(lowerEnergyThreshold_long); + Serial.print(" upperEnergyThreshold_long = "); + Serial.println(upperEnergyThreshold_long); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +} + + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + +void convertTemperature() +{ + oneWire.reset(); + oneWire.write(SKIP_ROM); + oneWire.write(CONVERT_TEMPERATURE); +} + +int readTemperature() +{ + byte buf[9]; + int result; + + oneWire.reset(); + oneWire.write(SKIP_ROM); + oneWire.write(READ_SCRATCHPAD); + for(int i=0; i<9; i++) buf[i]=oneWire.read(); + if(oneWire.crc8(buf,8)==buf[8]) + { + result=(buf[1]<<8)|buf[0]; + // result is temperature x16, multiply by 6.25 to convert to temperature x100 + result=(result*6)+(result>>2); + } + else result=BAD_TEMPERATURE; + return result; +} + + +void send_rf_data() +// +// To avoid disturbance to the sampling process, the RFM12B needs to remain in its +// active state rather than being periodically put to sleep. +{ + // check whether it's ready to send, and an exit route if it gets stuck + int i = 0; + while (!rf12_canSend() && i<10) + { + rf12_recvDone(); + i++; + } + rf12_sendNow(0, &tx_data, sizeof tx_data); +} + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_4b.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_4b.ino new file mode 100644 index 0000000..845bd63 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_4b.ino @@ -0,0 +1,1118 @@ +/* Mk2_RFdatalog_4b.ino + * + * This sketch is for diverting surplus PV power to a dump load using a triac. + * Routine datalogging is also supported using the on-board RFM12B module. + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The integral voltage sensor is fed from one of the secondary coils of the + * transformer. Current is measured via Current Transformers at the + * CT1 and CT2 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. When the RFM12B module is + * in use, the display can only be used in conjunction with an extra pair of + * logic chips. These are ICs 3 and 4, which reduce the number of processor pins + * that are needed to drive the display. + * + * This sketch is based on the Mk2i PV Router code that I have posted in on the + * OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * September 2014: renamed as Mk2_RFdatalog_3, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - tidier initialisation of display logic in setup(); + * + * December 2014, upgraded to Mk2_RFdatalog_4: + * This sketch has been restructured in order to make better use of the ISR. All of + * the time-critical code is now contained within the ISR and its helper functions. + * Values for datalogging are transferred to the main code using a flag-based handshake + * mechanism. The diversion of surplus power can no longer be affected by slower + * activities which may be running in the main code such as Serial statements and RF. + * Temperature sensing is supported by re-allocating the "mode" port for this + * purpose. A pullup resistor (4K7 or similar) is required for the Dallas sensor. The + * output mode, i.e. NORMAL or ANTI_FLICKER, is now set at compile time. + * Also: + * - The ADC is now in free-running mode, at ~104 us per conversion. + * - a persistence check has been added for zero-crossing detection (polarityConfirmed) + * - a lowestNoOfSampleSetsPerMainsCycle check has been added, to detect any disturbances + * - Vrms has been added to the datalog payload (as Vrms x 100) + * - temperature has been added to the datalog payload (as degrees C x 100) + * - the phaseCal mechanism has been reinstated + * + * January 2016: renamed as Mk2_RFdatalog_4a, with a minor change in the ISR to reinstate + * the phaseCal calculation. Previously, this feature was having no effect because + * two assignment lines were in the wrong order. When measuring "real power", which + * is what this application does, the phaseCal refinement has very little effect even + * when correctly implemented, as it now is. + * Support for the RF69 RF module has also been added. + * + * January 2016: updated to Mk2_RFdatalog_4b: + * The variables to store copies of ADC results for use by the main code are now declared + * as "volatile" to remove any possibility of incorrect operation due to optimisation + * by the compiler. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#define RF69_COMPAT 0 // <-- include this line for the RFM12B +// #define RF69_COMPAT 1 // <-- include this line for the RF69 + + +#include +#include +#include // JeeLib is available at from: http://github.com/jcw/jeelib + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// ----------------------------------------------------- +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define SWEETZONE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// -------------------------- +// Dallas DS18B20 commands +#define SKIP_ROM 0xcc +#define CONVERT_TEMPERATURE 0x44 +#define READ_SCRATCHPAD 0xbe +#define BAD_TEMPERATURE 30000 // this value (300C) is sent if no sensor is present + +// ---------------- +// general literals +#define DATALOG_PERIOD_IN_MAINS_CYCLES 250 +// #define POST_DATALOG_EVENT_DELAY_MILLIS 40 +#define ANTI_CREEP_LIMIT 0 // <- to prevent the diverted energy total from 'creeping' + // in Joules per mains cycle (has no effect when set to 0) + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +// ------------------------------- +// definitions of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum triacStates {TRIAC_ON, TRIAC_OFF}; // the external trigger device is active low +enum outputModes {ANTI_FLICKER, NORMAL}; // retained for compatibility with previous versions. + +// ---- Output mode selection ----- +enum outputModes outputMode = ANTI_FLICKER; // <- needs to be set here unless an +//enum outputModes outputMode = NORMAL; // external switch is in use + +/* -------------------------------------- + * RF configuration (for the RFM12B module) + * frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ + */ +#define freq RF12_433MHZ + +const int nodeID = 10; // RFM12B node ID +const int networkGroup = 210; // wireless network group - needs to be same for all nodes +const int UNO = 1; // for when the processor contains the UNO bootloader. + +typedef struct { + int powerAtSupplyPoint_Watts; // import = +ve, to match OEM convention + int divertedEnergyTotal_Wh; // always positive + int Vrms_times100; + int temperature_times100; +} Tx_struct; +Tx_struct tx_data; + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +// D2 is for the RFM12B +const byte tempSensorPin = 3; // <-- the "mode" port +const byte outputForTrigger = 4; +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +// D10 is for the RFM12B +// D11 is for the RFM12B +// D12 is for the RFM12B +// D13 is for the RFM12B + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + +const byte delayBeforeSerialStarts = 5; // in seconds, to allow Serial window to be opened +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +/* ------------------------------------------------------------------------------------- + * Global variables that are used in multiple blocks so cannot be static. + * For integer maths, many variables need to be 'long' + */ +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long energyInBucket_long; // in Integer Energy Units (for controlling the dump-load) +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long lowerEnergyThreshold_long; // for turning load off +long upperEnergyThreshold_long; // for turning load on +int phaseCal_grid_int; // to avoid the need for floating-point maths +int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF + +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; +float IEUtoJoulesConversion_CT1; + +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- not wise to exceeed 0.4 + +long sumP_forEnergyBucket; // for per-cycle summation of 'real power' +long sumP_diverted; // for per-cycle summation of diverted power +long sumP_atSupplyPoint; // for summation of 'real power' values during datalog period +long sum_Vsquared; // for summation of V^2 values during datalog period +int sampleSetsDuringThisCycle; // for counting the sample sets during each mains cycle +long sampleSetsDuringThisDatalogPeriod; // for counting the sample sets during each datalogging period + +long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) +long lastSampleVminusDC_long; // for the phaseCal algorithm +byte cycleCountForDatalogging = 0; +long sampleVminusDC_long; +long requiredExportPerMainsCycle_inIEU; + +// for interaction between the main code and the ISR +volatile boolean datalogEventPending = false; +volatile boolean newMainsCycle = false; +volatile long copyOf_sumP_atSupplyPoint; +volatile long copyOf_sum_Vsquared; +volatile long copyOf_divertedEnergyTotal_Wh; +volatile int copyOf_lowestNoOfSampleSetsPerMainsCycle; +volatile long copyOf_sampleSetsDuringThisDatalogPeriod; + +// For temperature sensing +OneWire oneWire(tempSensorPin); +int tempTimes100; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define POLARITY_CHECK_MAXCOUNT 2 +enum polarities polarityNow; +enum polarities polarityConfirmed; // for zero-crossing detection +enum polarities polarityConfirmedOfLastSampleV; // for zero-crossing detection + +// For a mechanism to check the integrity of this code structure +int lowestNoOfSampleSetsPerMainsCycle; +unsigned long timeAtLastDelay; + + +// Calibration values (not important for the Router's basic operation) +//------------------- +// For accurate calculation of real power/energy, two calibration values are +// used: powerCal and phaseCal. With most hardware, the default values are +// likely to work fine without need for change. A full explanation of each +// of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool_2chan.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. Any pre-built system that I supply will have been +// checked with this tool to ensure that the input sensors are working correctly. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 230V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 230V AC has a peak-to-peak amplitude of 651V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +// for 3.3V operation, the optimum value is generally around 0.044 +// for 5V operation, the optimum value is generally around 0.072 +// +const float powerCal_grid = 0.072; +const float powerCal_diverted = 0.073; + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. This mechanism can be used to offset any difference in +// phase delay between the voltage and current sensors. The algorithm interpolates +// between the most recent pair of voltage samples according to the phaseCal value. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value is used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. +// +// The calculation for real power is very insensitive to the value of phaseCal. +// When a "real power" calculation is used to determine how much surplus energy +// is available for diversion, a nominal value such as 1.0 is generally thought +// to be sufficient for this purpose. +// +const float phaseCal_grid = 1.0; +const float phaseCal_diverted = 1.0; + + +// For datalogging purposes, voltageCal has been included too. When running at +// 230 V AC, the range of ADC values will be similar to the actual range of volts, +// so the optimal value for this cal factor will be close to unity. +// +const float voltageCal = 1.0; + + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of this array is for the decimal point status. +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +volatile boolean EDD_isActive = false; // energy diversion detection +//volatile boolean EDD_isActive = true; // energy diversion detection + + +void setup() +{ + pinMode(outputForTrigger, OUTPUT); + digitalWrite (outputForTrigger, TRIAC_OFF); // the external trigger is active low + + delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_RFdatalog_4b.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, calibration values that are supplied in floating point + // form need to be rescaled. + // + phaseCal_grid_int = phaseCal_grid * 256; // for integer maths + phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail just before the energy bucket is updated at the start + // of each new mains cycle. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone's value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1): + capacityOfEnergyBucket_long = + (long)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up + + IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; // may be useful + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled in a known way. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_diverted); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.println ("ADC mode: free-running"); + + // Set up the ADC to be free-running + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + configureParamsForSelectedOutputMode(); + Serial.println ("----"); + + convertTemperature(); // start initial temperature conversion + + rf12_initialize(nodeID, freq, networkGroup); // initialize RF +// rf12_sleep(RF12_SLEEP); <- the RFM12B now stays awake throughout + + convertTemperature(); // start initial temperature conversion +} + +/* None of the workload in loop() is time-critical. All the processing of + * ADC data is done within the ISR. + */ +void loop() +{ +// unsigned long timeNow = millis(); + static byte perSecondTimer = 0; +// + // The ISR provides a 50 Hz 'tick' which the main code is free to use. + if (newMainsCycle) + { + newMainsCycle = false; + perSecondTimer++; + + if(perSecondTimer >= CYCLES_PER_SECOND) + { + perSecondTimer = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // Clear the accumulators for diverted energy. These are the "genuine" + // accumulators that are used by ISR rather than the copies that are + // regularly made available for use by the main code. + // + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); // this timing is not critical so does not need to be in the ISR + } + } + + if (datalogEventPending) + { + datalogEventPending= false; + tx_data.powerAtSupplyPoint_Watts = copyOf_sumP_atSupplyPoint * powerCal_grid / copyOf_sampleSetsDuringThisDatalogPeriod; + tx_data.powerAtSupplyPoint_Watts *= -1; // to match the OEM convention (import is =ve; export is -ve) + tx_data.divertedEnergyTotal_Wh = copyOf_divertedEnergyTotal_Wh; + tx_data.Vrms_times100 = (int)(100 * voltageCal * sqrt(copyOf_sum_Vsquared / copyOf_sampleSetsDuringThisDatalogPeriod)); + tx_data.temperature_times100 = readTemperature(); + send_rf_data(); + + Serial.print("datalog event: grid power "); Serial.print(tx_data.powerAtSupplyPoint_Watts); + Serial.print(", diverted energy (Wh) "); Serial.print(tx_data.divertedEnergyTotal_Wh); + Serial.print(", Vrms "); Serial.print((float)tx_data.Vrms_times100 / 100); + Serial.print(", temperature "); Serial.print((float)tx_data.temperature_times100 / 100); + Serial.print(", (minSampleSets/MC "); Serial.print(copyOf_lowestNoOfSampleSetsPerMainsCycle); + Serial.print(", #ofSampleSets "); Serial.print(copyOf_sampleSetsDuringThisDatalogPeriod); + Serial.println(')'); +// delay(POST_DATALOG_EVENT_DELAY_MILLIS); + convertTemperature(); // for use next time around + } + +/* + // occasional delays should not affect the operation of this revised code structure. + if (timeNow - timeAtLastDelay > 1000) + { + delay(100); + Serial.println("100ms delay"); + timeAtLastDelay = timeNow; + } +*/ +} + + +ISR(ADC_vect) +/* + * This Interrupt Service Routine looks after the acquisition and processing of + * raw samples from the ADC sub-processor. By means of various helper functions, all of + * the time-critical activities are processed within the ISR. The main code is notified + * by means of a flag when fresh copies of loggable data are available. + */ +{ + static unsigned char sample_index = 0; + int rawSample; + long sampleIminusDC; + long phaseShiftedSampleVminusDC; + long filtV_div4; + long filtI_div4; + long instP; + long inst_Vsquared; + + switch(sample_index) + { + case 0: + rawSample = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // the conversion for I_grid is already under way + sample_index++; // increment the control flag + // + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + sampleVminusDC_long = ((long)rawSample<<8) - DCoffset_V_long; + if(sampleVminusDC_long > 0) { + polarityNow = POSITIVE; } + else { + polarityNow = NEGATIVE; } + confirmPolarity(); + // + checkProgress(); // deals with aspects that only occur at particular stages of each mains cycle + // + // for the Vrms calculation (for datalogging only) + filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (now x4096, or 2^12) + inst_Vsquared = inst_Vsquared>>12; // scaling is now x1 (V_ADC x I_ADC) + sum_Vsquared += inst_Vsquared; // cumulative V^2 (V_ADC x I_ADC) + sampleSetsDuringThisDatalogPeriod++; + // + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter +// lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + sampleSetsDuringThisCycle++; // for real power calculations + refreshDisplay(); + break; + case 1: + rawSample = ADC; // store the ADC value (this one is for Grid Current) + ADMUX = 0x40 + voltageSensor; // the conversion for I_diverted is already under way + sample_index++; // increment the control flag + // + // remove most of the DC offset from the current sample (the precise value does not matter) + sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8; + // + // phase-shift the voltage waveform so that it aligns with the grid current waveform + phaseShiftedSampleVminusDC = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + // + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + + sumP_forEnergyBucket+=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + sumP_atSupplyPoint +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + break; + case 2: + rawSample = ADC; // store the ADC value (this one is for Diverted Current) + ADMUX = 0x40 + currentSensor_grid; // the conversion for Voltage is already under way + sample_index = 0; // reset the control flag + // + // remove most of the DC offset from the current sample (the precise value does not matter) + sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8; + // + // phase-shift the voltage waveform so that it aligns with the diverted current waveform + phaseShiftedSampleVminusDC = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + // + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + break; + default: + sample_index = 0; // to prevent lockup (should never get here) + } +} + +/* ----------------------------------------------------------- + * Start of various helper functions which are used by the ISR + */ + +void checkProgress() +/* + * This routine is called by the ISR when each voltage sample becomes available. + * At the start of each new mains cycle, another helper function is called. + * All other processing is done within this function. + */ +{ + static enum triacStates nextStateOfTriac = TRIAC_OFF; + + if (polarityConfirmed == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + // The start of a new mains cycle, just after the +ve going zero-crossing point. + + // a simple routine for checking the performance of this new ISR structure + if (sampleSetsDuringThisCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisCycle; } + + processLatestContribution(); // for activities at the start of each new mains cycle + + } // end of processing that is specific to the first +ve Vsample in each mains cycle + + // still processing samples where the voltage is POSITIVE ... + // check to see whether the trigger device can now be reliably armed + // + if (sampleSetsDuringThisCycle == 5) // part way through the +ve half cycle + { + if (energyInBucket_long < lowerEnergyThreshold_long) { + // when below the lower threshold, always set the triac to "off" + nextStateOfTriac = TRIAC_OFF; } + else + if (energyInBucket_long > upperEnergyThreshold_long) { + // when above the upper threshold, always set the triac to "off" + nextStateOfTriac = TRIAC_ON; } + else { + } // leave the triac's state unchanged (hysteresis) + + // set the Arduino's output pin accordingly + digitalWrite(outputForTrigger, nextStateOfTriac); + + // update the Energy Diversion Detector + if (nextStateOfTriac == TRIAC_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + } + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000) + { + beyondStartUpPhase = true; + sumP_forEnergyBucket = 0; + sumP_atSupplyPoint; + sumP_diverted = 0; + sampleSetsDuringThisCycle = 0; + sampleSetsDuringThisDatalogPeriod = 0; + } + } + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // Half way through the mains cycle, just after the -ve going zero-crossing point. + // This is a convenient place to update the Low Pass Filter for DC-offset removal. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 230V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need for a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + +// checkOutputModeSelection(); // updates outputMode if the external switch is in use + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is negative +} // end of checkProgress() + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. It forms part of the ISR. + */ + static byte count = 0; + if (polarityNow != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count >= POLARITY_CHECK_MAXCOUNT) + { + count = 0; + polarityConfirmed = polarityNow; + } +} + + +void processLatestContribution() +/* + * This routine runs once per mains cycle. It forms part of the ISR. + */ +{ + newMainsCycle = true; // <-- a 50 Hz 'tick' for use by the main code + + // For the mechanism which controls the diversion of surplus power, the AVERAGE power + // at the 'grid' point during the previous mains cycle must be quantified. The first + // stage in this process is for the sum of all instantaneous power values to be divided + // by the number of sample sets that have contributed to its value. A similar operation + // is required for the diverted power data. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_for_energyBucket = sumP_forEnergyBucket / sampleSetsDuringThisCycle; + long realPower_diverted = sumP_diverted / sampleSetsDuringThisCycle; + // + // The per-mainsCycle variables can now be reset for ongoing use + sampleSetsDuringThisCycle = 0; + sumP_forEnergyBucket = 0; + sumP_diverted = 0; + + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Average power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variables below are CYCLES_PER_SECOND * (1/powerCal) times larger than + // their actual values in Joules. + // + long realEnergy_for_energyBucket = realPower_for_energyBucket; + long realEnergy_diverted = realPower_diverted; + + // The latest energy contribution from the grid connection point can now be added + // to the energy bucket which determines the state of the dump-load. + // + energyInBucket_long += realEnergy_for_energyBucket; + energyInBucket_long -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation + + // Apply max and min limits to the bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. To avoid the displayed + // value from creeping, any small contributions which are likely to be + // caused by noise are ignored. + // + if (realEnergy_diverted > antiCreepLimit_inIEUperMainsCycle) { + divertedEnergyRecent_IEU += realEnergy_diverted; } + + // Whole Watt-Hours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + /* At the end of each datalogging period, copies are made of the relevant variables + * for use by the main code. These variable are then reset for use during the next + * datalogging period. + */ + cycleCountForDatalogging ++; + if (cycleCountForDatalogging >= DATALOG_PERIOD_IN_MAINS_CYCLES ) + { + cycleCountForDatalogging = 0; + + copyOf_sumP_atSupplyPoint = sumP_atSupplyPoint; + copyOf_divertedEnergyTotal_Wh = divertedEnergyTotal_Wh; + copyOf_sum_Vsquared = sum_Vsquared; + copyOf_sampleSetsDuringThisDatalogPeriod = sampleSetsDuringThisDatalogPeriod; // (for diags only) + copyOf_lowestNoOfSampleSetsPerMainsCycle = lowestNoOfSampleSetsPerMainsCycle; // (for diags only) + + sumP_atSupplyPoint = 0; + sum_Vsquared = 0; + lowestNoOfSampleSetsPerMainsCycle = 999; + sampleSetsDuringThisDatalogPeriod = 0; + datalogEventPending = true; + } +} +/* End of helper functions which are used by the ISR + * ------------------------------------------------- + */ + +// this function changes the value of outputMode if the external switch is in use for this purpose +void checkOutputModeSelection() +{ + static byte count = 0; + int pinState; + // pinState = digitalRead(outputModeSelectorPin); <- pin re-allocated for Dallas sensor + if (pinState != outputMode) + { + count++; + } + if (count >= 20) + { + count = 0; + outputMode = (enum outputModes)pinState; // change the global variable + Serial.print ("outputMode selection changed to "); + if (outputMode == NORMAL) { + Serial.println ( "normal"); } + else { + Serial.println ( "anti-flicker"); } + + configureParamsForSelectedOutputMode(); + } +} + + + +void configureParamsForSelectedOutputMode() +/* + * retained for compatibility with previous versions + */ +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + upperEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" lowerEnergyThreshold_long = "); + Serial.println(lowerEnergyThreshold_long); + Serial.print(" upperEnergyThreshold_long = "); + Serial.println(upperEnergyThreshold_long); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +} + + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + +void convertTemperature() +{ + oneWire.reset(); + oneWire.write(SKIP_ROM); + oneWire.write(CONVERT_TEMPERATURE); +} + +int readTemperature() +{ + byte buf[9]; + int result; + + oneWire.reset(); + oneWire.write(SKIP_ROM); + oneWire.write(READ_SCRATCHPAD); + for(int i=0; i<9; i++) buf[i]=oneWire.read(); + if(oneWire.crc8(buf,8)==buf[8]) + { + result=(buf[1]<<8)|buf[0]; + // result is temperature x16, multiply by 6.25 to convert to temperature x100 + result=(result*6)+(result>>2); + } + else result=BAD_TEMPERATURE; + return result; +} + + +void send_rf_data() +// +// To avoid disturbance to the sampling process, the RFM12B needs to remain in its +// active state rather than being periodically put to sleep. +{ + // check whether it's ready to send, and an exit route if it gets stuck + int i = 0; + while (!rf12_canSend() && i<10) + { + rf12_recvDone(); + i++; + } + rf12_sendNow(0, &tx_data, sizeof tx_data); +} + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_5.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_5.ino new file mode 100644 index 0000000..34ba24f --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_5.ino @@ -0,0 +1,1134 @@ +/* Mk2_RFdatalog_5.ino + * + * This sketch is for diverting surplus PV power to a dump load using a triac + * or Solid State Relay. Routine datalogging is also supported using the + * on-board RF module (either RFM12B or RF69). + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The integral voltage sensor is fed from one of the secondary coils of the + * transformer. Current is measured via Current Transformers at the + * CT1 and CT2 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. When the RFM12B module is + * in use, the display can only be used in conjunction with an extra pair of + * logic chips. These are ICs 3 and 4, which reduce the number of processor pins + * that are needed to drive the display. + * + * This sketch is based on the Mk2i PV Router code that I have posted in on the + * OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * September 2014: renamed as Mk2_RFdatalog_3, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - tidier initialisation of display logic in setup(); + * + * December 2014, upgraded to Mk2_RFdatalog_4: + * This sketch has been restructured in order to make better use of the ISR. All of + * the time-critical code is now contained within the ISR and its helper functions. + * Values for datalogging are transferred to the main code using a flag-based handshake + * mechanism. The diversion of surplus power can no longer be affected by slower + * activities which may be running in the main code such as Serial statements and RF. + * Temperature sensing is supported by re-allocating the "mode" port for this + * purpose. A pullup resistor (4K7 or similar) is required for the Dallas sensor. The + * output mode, i.e. NORMAL or ANTI_FLICKER, is now set at compile time. + * Also: + * - The ADC is now in free-running mode, at ~104 us per conversion. + * - a persistence check has been added for zero-crossing detection (polarityConfirmed) + * - a lowestNoOfSampleSetsPerMainsCycle check has been added, to detect any disturbances + * - Vrms has been added to the datalog payload (as Vrms x 100) + * - temperature has been added to the datalog payload (as degrees C x 100) + * - the phaseCal mechanism has been reinstated + * + * January 2016: renamed as Mk2_RFdatalog_4a, with a minor change in the ISR to reinstate + * the phaseCal calculation. Previously, this feature was having no effect because + * two assignment lines were in the wrong order. When measuring "real power", which + * is what this application does, the phaseCal refinement has very little effect even + * when correctly implemented, as it now is. + * Support for the RF69 RF module has also been added. + * + * January 2016: updated to Mk2_RFdatalog_4b: + * The variables to store copies of ADC results for use by the main code are now declared + * as "volatile" to remove any possibility of incorrect operation due to optimisation + * by the compiler. + * + * February 2016: updated to Mk2_RFdatalog_5, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +// #define RF69_COMPAT 0 // <-- include this line for the RFM12B +#define RF69_COMPAT 1 // <-- include this line for the RF69 + + +#include +#include +#include // JeeLib is available at from: http://github.com/jcw/jeelib + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// ----------------------------------------------------- +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define WORKING_RANGE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// -------------------------- +// Dallas DS18B20 commands +#define SKIP_ROM 0xcc +#define CONVERT_TEMPERATURE 0x44 +#define READ_SCRATCHPAD 0xbe +#define BAD_TEMPERATURE 30000 // this value (300C) is sent if no sensor is present + +// ---------------- +// general literals +#define DATALOG_PERIOD_IN_MAINS_CYCLES 250 +// #define POST_DATALOG_EVENT_DELAY_MILLIS 40 +#define ANTI_CREEP_LIMIT 0 // <- to prevent the diverted energy total from 'creeping' + // in Joules per mains cycle (has no effect when set to 0) + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +// ------------------------------- +// definitions of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum loadStates {LOAD_ON, LOAD_OFF}; // the external trigger device is active low +enum outputModes {ANTI_FLICKER, NORMAL}; // retained for compatibility with previous versions. + +// ---- Output mode selection ----- +enum outputModes outputMode = ANTI_FLICKER; // <- needs to be set here unless an +//enum outputModes outputMode = NORMAL; // external switch is in use + +/* -------------------------------------- + * RF configuration (for the RFM12B module) + * frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ + */ +#define freq RF12_433MHZ + +const int nodeID = 10; // RFM12B node ID +const int networkGroup = 210; // wireless network group - needs to be same for all nodes +const int UNO = 1; // for when the processor contains the UNO bootloader. + +typedef struct { + int powerAtSupplyPoint_Watts; // import = +ve, to match OEM convention + int divertedEnergyTotal_Wh; // always positive + int Vrms_times100; + int temperature_times100; +} Tx_struct; +Tx_struct tx_data; + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +// D2 is for the RFM12B +const byte tempSensorPin = 3; // <-- the "mode" port +const byte outputForTrigger = 4; +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +// D10 is for the RFM12B +// D11 is for the RFM12B +// D12 is for the RFM12B +// D13 is for the RFM12B + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + +const byte delayBeforeSerialStarts = 5; // in seconds, to allow Serial window to be opened +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +/* ------------------------------------------------------------------------------------- + * Global variables that are used in multiple blocks so cannot be static. + * For integer maths, many variables need to be 'long' + */ +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long energyInBucket_long = 0; // in Integer Energy Units (for controlling the dump-load) +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long lowerEnergyThreshold_long; // for turning load off +long upperEnergyThreshold_long; // for turning load on +int phaseCal_grid_int; // to avoid the need for floating-point maths +int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF + +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; +float IEUtoJoulesConversion_CT1; + +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- not wise to exceeed 0.4 + +long sumP_forEnergyBucket; // for per-cycle summation of 'real power' +long sumP_diverted; // for per-cycle summation of diverted power +long sumP_atSupplyPoint; // for summation of 'real power' values during datalog period +long sum_Vsquared; // for summation of V^2 values during datalog period +int sampleSetsDuringThisCycle; // for counting the sample sets during each mains cycle +long sampleSetsDuringThisDatalogPeriod; // for counting the sample sets during each datalogging period + +long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) +long lastSampleVminusDC_long; // for the phaseCal algorithm +byte cycleCountForDatalogging = 0; +long sampleVminusDC_long; +long requiredExportPerMainsCycle_inIEU; + +// for interaction between the main code and the ISR +volatile boolean datalogEventPending = false; +volatile boolean newMainsCycle = false; +volatile long copyOf_sumP_atSupplyPoint; +volatile long copyOf_sum_Vsquared; +volatile long copyOf_divertedEnergyTotal_Wh; +volatile int copyOf_lowestNoOfSampleSetsPerMainsCycle; +volatile long copyOf_sampleSetsDuringThisDatalogPeriod; + +// For temperature sensing +OneWire oneWire(tempSensorPin); +int tempTimes100; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define PERSISTENCE_FOR_POLARITY_CHANGE 2 +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; // for zero-crossing detection +enum polarities polarityConfirmedOfLastSampleV; // for zero-crossing detection + +// For a mechanism to check the integrity of this code structure +int lowestNoOfSampleSetsPerMainsCycle; +unsigned long timeAtLastDelay; + + +// Calibration values (not important for the Router's basic operation) +//------------------- +// For accurate calculation of real power/energy, two calibration values are +// used: powerCal and phaseCal. With most hardware, the default values are +// likely to work fine without need for change. A full explanation of each +// of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool_2chan.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. Any pre-built system that I supply will have been +// checked with this tool to ensure that the input sensors are working correctly. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 230V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 230V AC has a peak-to-peak amplitude of 651V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +// for 3.3V operation, the optimum value is generally around 0.044 +// for 5V operation, the optimum value is generally around 0.072 +// +const float powerCal_grid = 0.072; +const float powerCal_diverted = 0.073; + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. This mechanism can be used to offset any difference in +// phase delay between the voltage and current sensors. The algorithm interpolates +// between the most recent pair of voltage samples according to the phaseCal value. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value is used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. +// +// The calculation for real power is very insensitive to the value of phaseCal. +// When a "real power" calculation is used to determine how much surplus energy +// is available for diversion, a nominal value such as 1.0 is generally thought +// to be sufficient for this purpose. +// +const float phaseCal_grid = 1.0; +const float phaseCal_diverted = 1.0; + + +// For datalogging purposes, voltageCal has been included too. When running at +// 230 V AC, the range of ADC values will be similar to the actual range of volts, +// so the optimal value for this cal factor will be close to unity. +// +const float voltageCal = 1.0; + + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of this array is for the decimal point status. +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +volatile boolean EDD_isActive = false; // energy diversion detection +//volatile boolean EDD_isActive = true; // energy diversion detection + + +void setup() +{ + pinMode(outputForTrigger, OUTPUT); + digitalWrite (outputForTrigger, LOAD_OFF); // the external trigger is active low + + delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_RFdatalog_5.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, calibration values that are supplied in floating point + // form need to be rescaled. + // + phaseCal_grid_int = phaseCal_grid * 256; // for integer maths + phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail just before the energy bucket is updated at the start + // of each new mains cycle. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone's value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1): + capacityOfEnergyBucket_long = + (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + + IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; // may be useful + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled in a known way. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_diverted); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.println ("ADC mode: free-running"); + + // Set up the ADC to be free-running + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + configureParamsForSelectedOutputMode(); + Serial.println ("----"); + + convertTemperature(); // start initial temperature conversion + + rf12_initialize(nodeID, freq, networkGroup); // initialize RF +// rf12_sleep(RF12_SLEEP); <- the RFM12B now stays awake throughout + + convertTemperature(); // start initial temperature conversion +} + +/* None of the workload in loop() is time-critical. All the processing of + * ADC data is done within the ISR. + */ +void loop() +{ +// unsigned long timeNow = millis(); + static byte perSecondTimer = 0; +// + // The ISR provides a 50 Hz 'tick' which the main code is free to use. + if (newMainsCycle) + { + newMainsCycle = false; + perSecondTimer++; + + if(perSecondTimer >= CYCLES_PER_SECOND) + { + perSecondTimer = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // Clear the accumulators for diverted energy. These are the "genuine" + // accumulators that are used by ISR rather than the copies that are + // regularly made available for use by the main code. + // + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); // this timing is not critical so does not need to be in the ISR + } + } + + if (datalogEventPending) + { + datalogEventPending= false; + tx_data.powerAtSupplyPoint_Watts = copyOf_sumP_atSupplyPoint * powerCal_grid / copyOf_sampleSetsDuringThisDatalogPeriod; + tx_data.powerAtSupplyPoint_Watts *= -1; // to match the OEM convention (import is =ve; export is -ve) + tx_data.divertedEnergyTotal_Wh = copyOf_divertedEnergyTotal_Wh; + tx_data.Vrms_times100 = (int)(100 * voltageCal * sqrt(copyOf_sum_Vsquared / copyOf_sampleSetsDuringThisDatalogPeriod)); + tx_data.temperature_times100 = readTemperature(); + send_rf_data(); + + Serial.print("datalog event: grid power "); Serial.print(tx_data.powerAtSupplyPoint_Watts); + Serial.print(", diverted energy (Wh) "); Serial.print(tx_data.divertedEnergyTotal_Wh); + Serial.print(", Vrms "); Serial.print((float)tx_data.Vrms_times100 / 100); + Serial.print(", temperature "); Serial.print((float)tx_data.temperature_times100 / 100); + Serial.print(", (minSampleSets/MC "); Serial.print(copyOf_lowestNoOfSampleSetsPerMainsCycle); + Serial.print(", #ofSampleSets "); Serial.print(copyOf_sampleSetsDuringThisDatalogPeriod); + Serial.println(')'); +// delay(POST_DATALOG_EVENT_DELAY_MILLIS); + convertTemperature(); // for use next time around + } + +/* + // occasional delays should not affect the operation of this revised code structure. + if (timeNow - timeAtLastDelay > 1000) + { + delay(100); + Serial.println("100ms delay"); + timeAtLastDelay = timeNow; + } +*/ +} + + +ISR(ADC_vect) +/* + * This Interrupt Service Routine looks after the acquisition and processing of + * raw samples from the ADC sub-processor. By means of various helper functions, all of + * the time-critical activities are processed within the ISR. The main code is notified + * by means of a flag when fresh copies of loggable data are available. + */ +{ + static unsigned char sample_index = 0; + int rawSample; + long sampleIminusDC; + long phaseShiftedSampleVminusDC; + long filtV_div4; + long filtI_div4; + long instP; + long inst_Vsquared; + + switch(sample_index) + { + case 0: + rawSample = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // the conversion for I_grid is already under way + sample_index++; // increment the control flag + // + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + sampleVminusDC_long = ((long)rawSample<<8) - DCoffset_V_long; + if(sampleVminusDC_long > 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + // + checkProgress(); // deals with aspects that only occur at particular stages of each mains cycle + // + // for the Vrms calculation (for datalogging only) + filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (now x4096, or 2^12) + inst_Vsquared = inst_Vsquared>>12; // scaling is now x1 (V_ADC x I_ADC) + sum_Vsquared += inst_Vsquared; // cumulative V^2 (V_ADC x I_ADC) + sampleSetsDuringThisDatalogPeriod++; + // + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter +// lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + sampleSetsDuringThisCycle++; // for real power calculations + refreshDisplay(); + break; + case 1: + rawSample = ADC; // store the ADC value (this one is for Grid Current) + ADMUX = 0x40 + voltageSensor; // the conversion for I_diverted is already under way + sample_index++; // increment the control flag + // + // remove most of the DC offset from the current sample (the precise value does not matter) + sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8; + // + // phase-shift the voltage waveform so that it aligns with the grid current waveform + phaseShiftedSampleVminusDC = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + // + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + + sumP_forEnergyBucket+=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + sumP_atSupplyPoint +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + break; + case 2: + rawSample = ADC; // store the ADC value (this one is for Diverted Current) + ADMUX = 0x40 + currentSensor_grid; // the conversion for Voltage is already under way + sample_index = 0; // reset the control flag + // + // remove most of the DC offset from the current sample (the precise value does not matter) + sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8; + // + // phase-shift the voltage waveform so that it aligns with the diverted current waveform + phaseShiftedSampleVminusDC = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + // + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + break; + default: + sample_index = 0; // to prevent lockup (should never get here) + } +} + +/* ----------------------------------------------------------- + * Start of various helper functions which are used by the ISR + */ + +void checkProgress() +/* + * This routine is called by the ISR when each voltage sample becomes available. + * At the start of each new mains cycle, another helper function is called. + * All other processing is done within this function. + */ +{ + static enum loadStates nextStateOfLoad = LOAD_OFF; + + if (polarityConfirmed == POSITIVE) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + if (beyondStartUpPhase) + { + // The start of a new mains cycle, just after the +ve going zero-crossing point. + + // a simple routine for checking the performance of this new ISR structure + if (sampleSetsDuringThisCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisCycle; } + + processLatestContribution(); // for activities at the start of each new mains cycle + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000) + { + beyondStartUpPhase = true; + sumP_forEnergyBucket = 0; + sumP_atSupplyPoint; + sumP_diverted = 0; + sampleSetsDuringThisCycle = 0; // not yet dealt with for this cycle + sampleSetsDuringThisDatalogPeriod = 0; + // can't say "Go!" here 'cos we're in an ISR! + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + // check to see whether the trigger device can now be reliably armed + // + if (sampleSetsDuringThisCycle == 5) // part way through the +ve half cycle + { + if (beyondStartUpPhase) + { + if (energyInBucket_long < lowerEnergyThreshold_long) { + // when below the lower threshold, always set the load to "off" + nextStateOfLoad = LOAD_OFF; } + else + if (energyInBucket_long > upperEnergyThreshold_long) { + // when above the upper threshold, always set the load to "off" + nextStateOfLoad = LOAD_ON; } + else { + } // leave the load's state unchanged (hysteresis) + + // set the Arduino's output pin accordingly + digitalWrite(outputForTrigger, nextStateOfLoad); + + // update the Energy Diversion Detector + if (nextStateOfLoad == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + } + } + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12); + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 230V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need for a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + +// checkOutputModeSelection(); // updates outputMode if the external switch is in use + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is negative +} // end of checkProgress() + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count > PERSISTENCE_FOR_POLARITY_CHANGE) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + + +void processLatestContribution() +/* + * This routine runs once per mains cycle. It forms part of the ISR. + */ +{ + newMainsCycle = true; // <-- a 50 Hz 'tick' for use by the main code + + // For the mechanism which controls the diversion of surplus power, the AVERAGE power + // at the 'grid' point during the previous mains cycle must be quantified. The first + // stage in this process is for the sum of all instantaneous power values to be divided + // by the number of sample sets that have contributed to its value. A similar operation + // is required for the diverted power data. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_for_energyBucket = sumP_forEnergyBucket / sampleSetsDuringThisCycle; + long realPower_diverted = sumP_diverted / sampleSetsDuringThisCycle; + // + // The per-mainsCycle variables can now be reset for ongoing use + sampleSetsDuringThisCycle = 0; + sumP_forEnergyBucket = 0; + sumP_diverted = 0; + + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Average power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variables below are CYCLES_PER_SECOND * (1/powerCal) times larger than + // their actual values in Joules. + // + long realEnergy_for_energyBucket = realPower_for_energyBucket; + long realEnergy_diverted = realPower_diverted; + + // The latest energy contribution from the grid connection point can now be added + // to the energy bucket which determines the state of the dump-load. + // + energyInBucket_long += realEnergy_for_energyBucket; + energyInBucket_long -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation + + // Apply max and min limits to the bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. To avoid the displayed + // value from creeping, any small contributions which are likely to be + // caused by noise are ignored. + // + if (realEnergy_diverted > antiCreepLimit_inIEUperMainsCycle) { + divertedEnergyRecent_IEU += realEnergy_diverted; } + + // Whole Watt-Hours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + /* At the end of each datalogging period, copies are made of the relevant variables + * for use by the main code. These variable are then reset for use during the next + * datalogging period. + */ + cycleCountForDatalogging ++; + if (cycleCountForDatalogging >= DATALOG_PERIOD_IN_MAINS_CYCLES ) + { + cycleCountForDatalogging = 0; + + copyOf_sumP_atSupplyPoint = sumP_atSupplyPoint; + copyOf_divertedEnergyTotal_Wh = divertedEnergyTotal_Wh; + copyOf_sum_Vsquared = sum_Vsquared; + copyOf_sampleSetsDuringThisDatalogPeriod = sampleSetsDuringThisDatalogPeriod; // (for diags only) + copyOf_lowestNoOfSampleSetsPerMainsCycle = lowestNoOfSampleSetsPerMainsCycle; // (for diags only) + + sumP_atSupplyPoint = 0; + sum_Vsquared = 0; + lowestNoOfSampleSetsPerMainsCycle = 999; + sampleSetsDuringThisDatalogPeriod = 0; + datalogEventPending = true; + } +} +/* End of helper functions which are used by the ISR + * ------------------------------------------------- + */ + +// this function changes the value of outputMode if the external switch is in use for this purpose +void checkOutputModeSelection() +{ + static byte count = 0; + int pinState; + // pinState = digitalRead(outputModeSelectorPin); <- pin re-allocated for Dallas sensor + if (pinState != outputMode) + { + count++; + } + if (count >= 20) + { + count = 0; + outputMode = (enum outputModes)pinState; // change the global variable + Serial.print ("outputMode selection changed to "); + if (outputMode == NORMAL) { + Serial.println ( "normal"); } + else { + Serial.println ( "anti-flicker"); } + + configureParamsForSelectedOutputMode(); + } +} + + + +void configureParamsForSelectedOutputMode() +/* + * retained for compatibility with previous versions + */ +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + upperEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" lowerEnergyThreshold_long = "); + Serial.println(lowerEnergyThreshold_long); + Serial.print(" upperEnergyThreshold_long = "); + Serial.println(upperEnergyThreshold_long); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +} + + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + +void convertTemperature() +{ + oneWire.reset(); + oneWire.write(SKIP_ROM); + oneWire.write(CONVERT_TEMPERATURE); +} + +int readTemperature() +{ + byte buf[9]; + int result; + + oneWire.reset(); + oneWire.write(SKIP_ROM); + oneWire.write(READ_SCRATCHPAD); + for(int i=0; i<9; i++) buf[i]=oneWire.read(); + if(oneWire.crc8(buf,8)==buf[8]) + { + result=(buf[1]<<8)|buf[0]; + // result is temperature x16, multiply by 6.25 to convert to temperature x100 + result=(result*6)+(result>>2); + } + else result=BAD_TEMPERATURE; + return result; +} + + +void send_rf_data() +// +// To avoid disturbance to the sampling process, the RFM12B needs to remain in its +// active state rather than being periodically put to sleep. +{ + // check whether it's ready to send, and an exit route if it gets stuck + int i = 0; + while (!rf12_canSend() && i<10) + { + rf12_recvDone(); + i++; + } + rf12_sendNow(0, &tx_data, sizeof tx_data); +} + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_5a.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_5a.ino new file mode 100644 index 0000000..e4c6513 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_5a.ino @@ -0,0 +1,1156 @@ +/* Mk2_RFdatalog_5a.ino + * + * This sketch is for diverting surplus PV power to a dump load using a triac + * or Solid State Relay. Routine datalogging is also supported using the + * on-board RF module (either RFM12B or RF69). + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The integral voltage sensor is fed from one of the secondary coils of the + * transformer. Current is measured via Current Transformers at the + * CT1 and CT2 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. When the RFM12B module is + * in use, the display can only be used in conjunction with an extra pair of + * logic chips. These are ICs 3 and 4, which reduce the number of processor pins + * that are needed to drive the display. + * + * This sketch is based on the Mk2i PV Router code that I have posted in on the + * OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * September 2014: renamed as Mk2_RFdatalog_3, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - tidier initialisation of display logic in setup(); + * + * December 2014, upgraded to Mk2_RFdatalog_4: + * This sketch has been restructured in order to make better use of the ISR. All of + * the time-critical code is now contained within the ISR and its helper functions. + * Values for datalogging are transferred to the main code using a flag-based handshake + * mechanism. The diversion of surplus power can no longer be affected by slower + * activities which may be running in the main code such as Serial statements and RF. + * Temperature sensing is supported by re-allocating the "mode" port for this + * purpose. A pullup resistor (4K7 or similar) is required for the Dallas sensor. The + * output mode, i.e. NORMAL or ANTI_FLICKER, is now set at compile time. + * Also: + * - The ADC is now in free-running mode, at ~104 us per conversion. + * - a persistence check has been added for zero-crossing detection (polarityConfirmed) + * - a lowestNoOfSampleSetsPerMainsCycle check has been added, to detect any disturbances + * - Vrms has been added to the datalog payload (as Vrms x 100) + * - temperature has been added to the datalog payload (as degrees C x 100) + * - the phaseCal mechanism has been reinstated + * + * January 2016: renamed as Mk2_RFdatalog_4a, with a minor change in the ISR to reinstate + * the phaseCal calculation. Previously, this feature was having no effect because + * two assignment lines were in the wrong order. When measuring "real power", which + * is what this application does, the phaseCal refinement has very little effect even + * when correctly implemented, as it now is. + * Support for the RF69 RF module has also been added. + * + * January 2016: updated to Mk2_RFdatalog_4b: + * The variables to store copies of ADC results for use by the main code are now declared + * as "volatile" to remove any possibility of incorrect operation due to optimisation + * by the compiler. + * + * February 2016: updated to Mk2_RFdatalog_5, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * March 2016: updated to Mk2_RFdatalog_5a, with this change: + * - RF capability made switchable so that the code will continue to run + * when an RF module is not fitted. Dataloging can then take place + * via the Serial port. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include +#include + +#define RF_PRESENT // <- this line should be commented out if the RFM12B module is not present + +#ifdef RF_PRESENT +#define RF69_COMPAT 0 // for the RFM12B +// #define RF69_COMPAT 1 // for the RF69 +#include +#endif + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// ----------------------------------------------------- +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define WORKING_RANGE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// -------------------------- +// Dallas DS18B20 commands +#define SKIP_ROM 0xcc +#define CONVERT_TEMPERATURE 0x44 +#define READ_SCRATCHPAD 0xbe +#define BAD_TEMPERATURE 30000 // this value (300C) is sent if no sensor is present + +// ---------------- +// general literals +#define DATALOG_PERIOD_IN_MAINS_CYCLES 250 +// #define POST_DATALOG_EVENT_DELAY_MILLIS 40 +#define ANTI_CREEP_LIMIT 0 // <- to prevent the diverted energy total from 'creeping' + // in Joules per mains cycle (has no effect when set to 0) + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +// ------------------------------- +// definitions of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum loadStates {LOAD_ON, LOAD_OFF}; // the external trigger device is active low +enum outputModes {ANTI_FLICKER, NORMAL}; // retained for compatibility with previous versions. + +// ---- Output mode selection ----- +enum outputModes outputMode = ANTI_FLICKER; // <- needs to be set here unless an +//enum outputModes outputMode = NORMAL; // external switch is in use + +/* -------------------------------------- + * RF configuration (for the RFM12B module) + * frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ + */ +#ifdef RF_PRESENT +#define freq RF12_868MHZ + +const int nodeID = 10; // RFM12B node ID +const int networkGroup = 210; // wireless network group - needs to be same for all nodes +const int UNO = 1; // for when the processor contains the UNO bootloader. +#endif + +typedef struct { + int powerAtSupplyPoint_Watts; // import = +ve, to match OEM convention + int divertedEnergyTotal_Wh; // always positive + int Vrms_times100; + int temperature_times100; +} Tx_struct; +Tx_struct tx_data; + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +// D2 is for the RFM12B +const byte tempSensorPin = 3; // <-- the "mode" port +const byte outputForTrigger = 4; +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +// D10 is for the RFM12B +// D11 is for the RFM12B +// D12 is for the RFM12B +// D13 is for the RFM12B + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + +const byte delayBeforeSerialStarts = 5; // in seconds, to allow Serial window to be opened +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +/* ------------------------------------------------------------------------------------- + * Global variables that are used in multiple blocks so cannot be static. + * For integer maths, many variables need to be 'long' + */ +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long energyInBucket_long = 0; // in Integer Energy Units (for controlling the dump-load) +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long lowerEnergyThreshold_long; // for turning load off +long upperEnergyThreshold_long; // for turning load on +int phaseCal_grid_int; // to avoid the need for floating-point maths +int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF + +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; +float IEUtoJoulesConversion_CT1; + +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- not wise to exceeed 0.4 + +long sumP_forEnergyBucket; // for per-cycle summation of 'real power' +long sumP_diverted; // for per-cycle summation of diverted power +long sumP_atSupplyPoint; // for summation of 'real power' values during datalog period +long sum_Vsquared; // for summation of V^2 values during datalog period +int sampleSetsDuringThisCycle; // for counting the sample sets during each mains cycle +long sampleSetsDuringThisDatalogPeriod; // for counting the sample sets during each datalogging period + +long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) +long lastSampleVminusDC_long; // for the phaseCal algorithm +byte cycleCountForDatalogging = 0; +long sampleVminusDC_long; +long requiredExportPerMainsCycle_inIEU; + +// for interaction between the main code and the ISR +volatile boolean datalogEventPending = false; +volatile boolean newMainsCycle = false; +volatile long copyOf_sumP_atSupplyPoint; +volatile long copyOf_sum_Vsquared; +volatile long copyOf_divertedEnergyTotal_Wh; +volatile int copyOf_lowestNoOfSampleSetsPerMainsCycle; +volatile long copyOf_sampleSetsDuringThisDatalogPeriod; + +// For temperature sensing +OneWire oneWire(tempSensorPin); +int tempTimes100; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define PERSISTENCE_FOR_POLARITY_CHANGE 2 +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; // for zero-crossing detection +enum polarities polarityConfirmedOfLastSampleV; // for zero-crossing detection + +// For a mechanism to check the integrity of this code structure +int lowestNoOfSampleSetsPerMainsCycle; +unsigned long timeAtLastDelay; + + +// Calibration values (not important for the Router's basic operation) +//------------------- +// For accurate calculation of real power/energy, two calibration values are +// used: powerCal and phaseCal. With most hardware, the default values are +// likely to work fine without need for change. A full explanation of each +// of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool_2chan.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. Any pre-built system that I supply will have been +// checked with this tool to ensure that the input sensors are working correctly. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 230V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 230V AC has a peak-to-peak amplitude of 651V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +// for 3.3V operation, the optimum value is generally around 0.044 +// for 5V operation, the optimum value is generally around 0.072 +// +const float powerCal_grid = 0.072; +const float powerCal_diverted = 0.073; + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. This mechanism can be used to offset any difference in +// phase delay between the voltage and current sensors. The algorithm interpolates +// between the most recent pair of voltage samples according to the phaseCal value. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value is used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. +// +// The calculation for real power is very insensitive to the value of phaseCal. +// When a "real power" calculation is used to determine how much surplus energy +// is available for diversion, a nominal value such as 1.0 is generally thought +// to be sufficient for this purpose. +// +const float phaseCal_grid = 1.0; +const float phaseCal_diverted = 1.0; + + +// For datalogging purposes, voltageCal has been included too. When running at +// 230 V AC, the range of ADC values will be similar to the actual range of volts, +// so the optimal value for this cal factor will be close to unity. +// +const float voltageCal = 1.0; + + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of this array is for the decimal point status. +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +volatile boolean EDD_isActive = false; // energy diversion detection +//volatile boolean EDD_isActive = true; // energy diversion detection + + +void setup() +{ + pinMode(outputForTrigger, OUTPUT); + digitalWrite (outputForTrigger, LOAD_OFF); // the external trigger is active low + + delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_RFdatalog_5a.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, calibration values that are supplied in floating point + // form need to be rescaled. + // + phaseCal_grid_int = phaseCal_grid * 256; // for integer maths + phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail just before the energy bucket is updated at the start + // of each new mains cycle. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone's value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1): + capacityOfEnergyBucket_long = + (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + + IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; // may be useful + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled in a known way. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_diverted); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.println ("ADC mode: free-running"); + + // Set up the ADC to be free-running + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + configureParamsForSelectedOutputMode(); + Serial.println ("----"); + + convertTemperature(); // start initial temperature conversion + + Serial.print ("RF capability "); + +#ifdef RF_PRESENT + Serial.print ("IS present, freq = "); + if (freq == RF12_433MHZ) { Serial.println ("433 MHz"); } + if (freq == RF12_868MHZ) { Serial.println ("868 MHz"); } + rf12_initialize(nodeID, freq, networkGroup); // initialize RF +#else + Serial.println ("is NOT present"); +#endif + + convertTemperature(); // start initial temperature conversion +} + +/* None of the workload in loop() is time-critical. All the processing of + * ADC data is done within the ISR. + */ +void loop() +{ +// unsigned long timeNow = millis(); + static byte perSecondTimer = 0; +// + // The ISR provides a 50 Hz 'tick' which the main code is free to use. + if (newMainsCycle) + { + newMainsCycle = false; + perSecondTimer++; + + if(perSecondTimer >= CYCLES_PER_SECOND) + { + perSecondTimer = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // Clear the accumulators for diverted energy. These are the "genuine" + // accumulators that are used by ISR rather than the copies that are + // regularly made available for use by the main code. + // + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); // this timing is not critical so does not need to be in the ISR + } + } + + if (datalogEventPending) + { + datalogEventPending= false; + tx_data.powerAtSupplyPoint_Watts = copyOf_sumP_atSupplyPoint * powerCal_grid / copyOf_sampleSetsDuringThisDatalogPeriod; + tx_data.powerAtSupplyPoint_Watts *= -1; // to match the OEM convention (import is =ve; export is -ve) + tx_data.divertedEnergyTotal_Wh = copyOf_divertedEnergyTotal_Wh; + tx_data.Vrms_times100 = (int)(100 * voltageCal * sqrt(copyOf_sum_Vsquared / copyOf_sampleSetsDuringThisDatalogPeriod)); + tx_data.temperature_times100 = readTemperature(); + +#ifdef RF_PRESENT + send_rf_data(); +#endif + + Serial.print("datalog event: grid power "); Serial.print(tx_data.powerAtSupplyPoint_Watts); + Serial.print(", diverted energy (Wh) "); Serial.print(tx_data.divertedEnergyTotal_Wh); + Serial.print(", Vrms "); Serial.print((float)tx_data.Vrms_times100 / 100); + Serial.print(", temperature "); Serial.print((float)tx_data.temperature_times100 / 100); + Serial.print(", (minSampleSets/MC "); Serial.print(copyOf_lowestNoOfSampleSetsPerMainsCycle); + Serial.print(", #ofSampleSets "); Serial.print(copyOf_sampleSetsDuringThisDatalogPeriod); + Serial.println(')'); +// delay(POST_DATALOG_EVENT_DELAY_MILLIS); + convertTemperature(); // for use next time around + } + +/* + // occasional delays should not affect the operation of this revised code structure. + if (timeNow - timeAtLastDelay > 1000) + { + delay(100); + Serial.println("100ms delay"); + timeAtLastDelay = timeNow; + } +*/ +} + + +ISR(ADC_vect) +/* + * This Interrupt Service Routine looks after the acquisition and processing of + * raw samples from the ADC sub-processor. By means of various helper functions, all of + * the time-critical activities are processed within the ISR. The main code is notified + * by means of a flag when fresh copies of loggable data are available. + */ +{ + static unsigned char sample_index = 0; + int rawSample; + long sampleIminusDC; + long phaseShiftedSampleVminusDC; + long filtV_div4; + long filtI_div4; + long instP; + long inst_Vsquared; + + switch(sample_index) + { + case 0: + rawSample = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // the conversion for I_grid is already under way + sample_index++; // increment the control flag + // + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + sampleVminusDC_long = ((long)rawSample<<8) - DCoffset_V_long; + if(sampleVminusDC_long > 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + // + checkProgress(); // deals with aspects that only occur at particular stages of each mains cycle + // + // for the Vrms calculation (for datalogging only) + filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (now x4096, or 2^12) + inst_Vsquared = inst_Vsquared>>12; // scaling is now x1 (V_ADC x I_ADC) + sum_Vsquared += inst_Vsquared; // cumulative V^2 (V_ADC x I_ADC) + sampleSetsDuringThisDatalogPeriod++; + // + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter +// lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + sampleSetsDuringThisCycle++; // for real power calculations + refreshDisplay(); + break; + case 1: + rawSample = ADC; // store the ADC value (this one is for Grid Current) + ADMUX = 0x40 + voltageSensor; // the conversion for I_diverted is already under way + sample_index++; // increment the control flag + // + // remove most of the DC offset from the current sample (the precise value does not matter) + sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8; + // + // phase-shift the voltage waveform so that it aligns with the grid current waveform + phaseShiftedSampleVminusDC = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + // + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + + sumP_forEnergyBucket+=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + sumP_atSupplyPoint +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + break; + case 2: + rawSample = ADC; // store the ADC value (this one is for Diverted Current) + ADMUX = 0x40 + currentSensor_grid; // the conversion for Voltage is already under way + sample_index = 0; // reset the control flag + // + // remove most of the DC offset from the current sample (the precise value does not matter) + sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8; + // + // phase-shift the voltage waveform so that it aligns with the diverted current waveform + phaseShiftedSampleVminusDC = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + // + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + break; + default: + sample_index = 0; // to prevent lockup (should never get here) + } +} + +/* ----------------------------------------------------------- + * Start of various helper functions which are used by the ISR + */ + +void checkProgress() +/* + * This routine is called by the ISR when each voltage sample becomes available. + * At the start of each new mains cycle, another helper function is called. + * All other processing is done within this function. + */ +{ + static enum loadStates nextStateOfLoad = LOAD_OFF; + + if (polarityConfirmed == POSITIVE) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + if (beyondStartUpPhase) + { + // The start of a new mains cycle, just after the +ve going zero-crossing point. + + // a simple routine for checking the performance of this new ISR structure + if (sampleSetsDuringThisCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisCycle; } + + processLatestContribution(); // for activities at the start of each new mains cycle + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000) + { + beyondStartUpPhase = true; + sumP_forEnergyBucket = 0; + sumP_atSupplyPoint; + sumP_diverted = 0; + sampleSetsDuringThisCycle = 0; // not yet dealt with for this cycle + sampleSetsDuringThisDatalogPeriod = 0; + // can't say "Go!" here 'cos we're in an ISR! + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + // check to see whether the trigger device can now be reliably armed + // + if (sampleSetsDuringThisCycle == 5) // part way through the +ve half cycle + { + if (beyondStartUpPhase) + { + if (energyInBucket_long < lowerEnergyThreshold_long) { + // when below the lower threshold, always set the load to "off" + nextStateOfLoad = LOAD_OFF; } + else + if (energyInBucket_long > upperEnergyThreshold_long) { + // when above the upper threshold, always set the load to "off" + nextStateOfLoad = LOAD_ON; } + else { + } // leave the load's state unchanged (hysteresis) + + // set the Arduino's output pin accordingly + digitalWrite(outputForTrigger, nextStateOfLoad); + + // update the Energy Diversion Detector + if (nextStateOfLoad == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + } + } + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12); + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 230V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need for a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + +// checkOutputModeSelection(); // updates outputMode if the external switch is in use + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is negative +} // end of checkProgress() + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count > PERSISTENCE_FOR_POLARITY_CHANGE) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + + +void processLatestContribution() +/* + * This routine runs once per mains cycle. It forms part of the ISR. + */ +{ + newMainsCycle = true; // <-- a 50 Hz 'tick' for use by the main code + + // For the mechanism which controls the diversion of surplus power, the AVERAGE power + // at the 'grid' point during the previous mains cycle must be quantified. The first + // stage in this process is for the sum of all instantaneous power values to be divided + // by the number of sample sets that have contributed to its value. A similar operation + // is required for the diverted power data. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_for_energyBucket = sumP_forEnergyBucket / sampleSetsDuringThisCycle; + long realPower_diverted = sumP_diverted / sampleSetsDuringThisCycle; + // + // The per-mainsCycle variables can now be reset for ongoing use + sampleSetsDuringThisCycle = 0; + sumP_forEnergyBucket = 0; + sumP_diverted = 0; + + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Average power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variables below are CYCLES_PER_SECOND * (1/powerCal) times larger than + // their actual values in Joules. + // + long realEnergy_for_energyBucket = realPower_for_energyBucket; + long realEnergy_diverted = realPower_diverted; + + // The latest energy contribution from the grid connection point can now be added + // to the energy bucket which determines the state of the dump-load. + // + energyInBucket_long += realEnergy_for_energyBucket; + energyInBucket_long -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation + + // Apply max and min limits to the bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. To avoid the displayed + // value from creeping, any small contributions which are likely to be + // caused by noise are ignored. + // + if (realEnergy_diverted > antiCreepLimit_inIEUperMainsCycle) { + divertedEnergyRecent_IEU += realEnergy_diverted; } + + // Whole Watt-Hours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + /* At the end of each datalogging period, copies are made of the relevant variables + * for use by the main code. These variable are then reset for use during the next + * datalogging period. + */ + cycleCountForDatalogging ++; + if (cycleCountForDatalogging >= DATALOG_PERIOD_IN_MAINS_CYCLES ) + { + cycleCountForDatalogging = 0; + + copyOf_sumP_atSupplyPoint = sumP_atSupplyPoint; + copyOf_divertedEnergyTotal_Wh = divertedEnergyTotal_Wh; + copyOf_sum_Vsquared = sum_Vsquared; + copyOf_sampleSetsDuringThisDatalogPeriod = sampleSetsDuringThisDatalogPeriod; // (for diags only) + copyOf_lowestNoOfSampleSetsPerMainsCycle = lowestNoOfSampleSetsPerMainsCycle; // (for diags only) + + sumP_atSupplyPoint = 0; + sum_Vsquared = 0; + lowestNoOfSampleSetsPerMainsCycle = 999; + sampleSetsDuringThisDatalogPeriod = 0; + datalogEventPending = true; + } +} +/* End of helper functions which are used by the ISR + * ------------------------------------------------- + */ + +// this function changes the value of outputMode if the external switch is in use for this purpose +void checkOutputModeSelection() +{ + static byte count = 0; + int pinState; + // pinState = digitalRead(outputModeSelectorPin); <- pin re-allocated for Dallas sensor + if (pinState != outputMode) + { + count++; + } + if (count >= 20) + { + count = 0; + outputMode = (enum outputModes)pinState; // change the global variable + Serial.print ("outputMode selection changed to "); + if (outputMode == NORMAL) { + Serial.println ( "normal"); } + else { + Serial.println ( "anti-flicker"); } + + configureParamsForSelectedOutputMode(); + } +} + + + +void configureParamsForSelectedOutputMode() +/* + * retained for compatibility with previous versions + */ +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + upperEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" lowerEnergyThreshold_long = "); + Serial.println(lowerEnergyThreshold_long); + Serial.print(" upperEnergyThreshold_long = "); + Serial.println(upperEnergyThreshold_long); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +} + + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + +void convertTemperature() +{ + oneWire.reset(); + oneWire.write(SKIP_ROM); + oneWire.write(CONVERT_TEMPERATURE); +} + +int readTemperature() +{ + byte buf[9]; + int result; + + oneWire.reset(); + oneWire.write(SKIP_ROM); + oneWire.write(READ_SCRATCHPAD); + for(int i=0; i<9; i++) buf[i]=oneWire.read(); + if(oneWire.crc8(buf,8)==buf[8]) + { + result=(buf[1]<<8)|buf[0]; + // result is temperature x16, multiply by 6.25 to convert to temperature x100 + result=(result*6)+(result>>2); + } + else result=BAD_TEMPERATURE; + return result; +} + + +#ifdef RF_PRESENT +void send_rf_data() +// +// To avoid disturbance to the sampling process, the RFM12B needs to remain in its +// active state rather than being periodically put to sleep. +{ + // check whether it's ready to send, and an exit route if it gets stuck + int i = 0; + while (!rf12_canSend() && i<10) + { + rf12_recvDone(); + i++; + } + rf12_sendNow(0, &tx_data, sizeof tx_data); +} +#endif + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_6.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_6.ino new file mode 100644 index 0000000..ad9598b --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_6.ino @@ -0,0 +1,1173 @@ +/* Mk2_RFdatalog_6.ino + * + * This sketch is for diverting suplus PV power to a dump load using a triac + * or Solid State Relay. Routine datalogging is also supported using the + * on-board RF module (either RFM12B or RF69). + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The integral voltage sensor is fed from one of the secondary coils of the + * transformer. Current is measured via Current Transformers at the + * CT1 and CT1 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. When the RFM12B module is + * in use, the display can only be used in conjunction with an extra pair of + * logic chips. These are ICs 3 and 4, which reduce the number of processor pins + * that are needed to drive the display. + * + * This sketch is based on the Mk2i PV Router code that I have posted in on the + * OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * September 2014: renamed as Mk2_RFdatalog_3, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - tidier initialisation of display logic in setup(); + * + * December 2014, upgraded to Mk2_RFdatalog_4: + * This sketch has been restructured in order to make better use of the ISR. All of + * the time-critical code is now contained within the ISR and its helper functions. + * Values for datalogging are transferred to the main code using a flag-based handshake + * mechanism. The diversion of surplus power can no longer be affected by slower + * activities which may be running in the main code such as Serial statements and RF. + * Temperature sensing is supported by re-allocating the "mode" port for this + * purpose. A pullup resistor (4K7 or similar) is required for the Dallas sensor. The + * output mode, i.e. NORMAL or ANTI_FLICKER, is now set at compile time. + * Also: + * - The ADC is now in free-running mode, at ~104 us per conversion. + * - a persistence check has been added for zero-crossing detection (polarityConfirmed) + * - a lowestNoOfSampleSetsPerMainsCycle check has been added, to detect any disturbances + * - Vrms has been added to the datalog payload (as Vrms x 100) + * - temperature has been added to the datalog payload (as degrees C x 100) + * - the phaseCal mechanism has been reinstated + * + * January 2016: renamed as Mk2_RFdatalog_4a, with a minor change in the ISR to reinstate + * the phaseCal calculation. Previously, this feature was having no effect because + * two assignment lines were in the wrong order. When measuring "real power", which + * is what this application does, the phaseCal refinement has very little effect even + * when correctly implemented, as it now is. + * Support for the RF69 RF module has also been added. + * + * January 2016: updated to Mk2_RFdatalog_4b: + * The variables to store copies of ADC results for use by the main code are now declared + * as "volatile" to remove any possibility of incorrect operation due to optimisation + * by the compiler. + * + * February 2016: updated to Mk2_RFdatalog_5, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * March 2016: updated to Mk2_RFdatalog_5a, with this change: + * - RF capability made switchable so that the code will continue to run + * when an RF module is not fitted. Dataloging can then take place + * via the Serial port. + * + * November 2020: updated to Mk2_RFdatalog_6, with these changes: + * - the parameter cycleCountForDatalogging is now an "int" rather that a "byte". This + * allows the datalogging period to be extended beyond 5 seconds without the counter + * running out of range. + * - diverted energy, as monitored by CT2, is now reported as an average power as well as + * the cumulative energy total for the current day. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include +#include + +#define RF_PRESENT // <- this line should be commented out if the RFM12B module is not present + +#ifdef RF_PRESENT +#define RF69_COMPAT 0 // for the RFM12B +// #define RF69_COMPAT 1 // for the RF69 +#include +#endif + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// ----------------------------------------------------- +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define WORKING_RANGE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// -------------------------- +// Dallas DS18B20 commands +#define SKIP_ROM 0xcc +#define CONVERT_TEMPERATURE 0x44 +#define READ_SCRATCHPAD 0xbe +#define BAD_TEMPERATURE 30000 // this value (300C) is sent if no sensor is present + +// ---------------- +// general literals +#define DATALOG_PERIOD_IN_MAINS_CYCLES 500 +// #define POST_DATALOG_EVENT_DELAY_MILLIS 40 +#define ANTI_CREEP_LIMIT 0 // <- to prevent the diverted energy total from 'creeping' + // in Joules per mains cycle (has no effect when set to 0) + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +// ------------------------------- +// definitions of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum loadStates {LOAD_ON, LOAD_OFF}; // the external trigger device is active low +enum outputModes {ANTI_FLICKER, NORMAL}; // retained for compatibility with previous versions. + +// ---- Output mode selection ----- +enum outputModes outputMode = ANTI_FLICKER; // <- needs to be set here unless an +//enum outputModes outputMode = NORMAL; // external switch is in use + +/* -------------------------------------- + * RF configuration (for the RFM12B module) + * frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ + */ +#ifdef RF_PRESENT +#define freq RF12_433MHZ + +const int nodeID = 10; // RFM12B node ID +const int networkGroup = 210; // wireless network group - needs to be same for all nodes +const int UNO = 1; // for when the processor contains the UNO bootloader. +#endif + +typedef struct { + int powerAtSupplyPoint_Watts; // import = +ve, to match OEM convention + int divertedEnergyTotal_Wh; // always positive + int divertedPower_Watts; // always positive + int Vrms_times100; + int temperature_times100; +} Tx_struct; +Tx_struct tx_data; + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +// D2 is for the RFM12B +const byte tempSensorPin = 3; // <-- the "mode" port +const byte outputForTrigger = 4; +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +// D10 is for the RFM12B +// D11 is for the RFM12B +// D12 is for the RFM12B +// D13 is for the RFM12B + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + +const byte delayBeforeSerialStarts = 5; // in seconds, to allow Serial window to be opened +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +/* ------------------------------------------------------------------------------------- + * Global variables that are used in multiple blocks so cannot be static. + * For integer maths, many variables need to be 'long' + */ +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long energyInBucket_long = 0; // in Integer Energy Units (for controlling the dump-load) +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long lowerEnergyThreshold_long; // for turning load off +long upperEnergyThreshold_long; // for turning load on +int phaseCal_grid_int; // to avoid the need for floating-point maths +int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF + +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; +float IEUtoJoulesConversion_CT1; + +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- not wise to exceeed 0.4 + +long sumP_forEnergyBucket; // for per-cycle summation of 'real power' +long sumP_forDivertedEnergy; // for per-cycle summation of diverted energy +long sumP_atSupplyPoint; // for summation of 'real power' values during datalog period +long sumP_forDivertedPower; // for summation of diverted power values during datalog period +long sum_Vsquared; // for summation of V^2 values during datalog period +int sampleSetsDuringThisCycle; // for counting the sample sets during each mains cycle +long sampleSetsDuringThisDatalogPeriod; // for counting the sample sets during each datalogging period + +long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) +long lastSampleVminusDC_long; // for the phaseCal algorithm +int cycleCountForDatalogging = 0; +long sampleVminusDC_long; +long requiredExportPerMainsCycle_inIEU; + +// for interaction between the main code and the ISR +volatile boolean datalogEventPending = false; +volatile boolean newMainsCycle = false; +volatile long copyOf_sumP_atSupplyPoint; +volatile long copyOf_sumP_forDivertedPower; +volatile long copyOf_sum_Vsquared; +volatile long copyOf_divertedEnergyTotal_Wh; +volatile int copyOf_lowestNoOfSampleSetsPerMainsCycle; +volatile long copyOf_sampleSetsDuringThisDatalogPeriod; + +// For temperature sensing +OneWire oneWire(tempSensorPin); +int tempTimes100; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define PERSISTENCE_FOR_POLARITY_CHANGE 2 +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; // for zero-crossing detection +enum polarities polarityConfirmedOfLastSampleV; // for zero-crossing detection + +// For a mechanism to check the integrity of this code structure +int lowestNoOfSampleSetsPerMainsCycle; +unsigned long timeAtLastDelay; + + +// Calibration values (not important for the Router's basic operation) +//------------------- +// For accurate calculation of real power/energy, two calibration values are +// used: powerCal and phaseCal. With most hardware, the default values are +// likely to work fine without need for change. A full explanation of each +// of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool_2chan.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. Any pre-built system that I supply will have been +// checked with this tool to ensure that the input sensors are working correctly. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 230V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 230V AC has a peak-to-peak amplitude of 651V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +// for 3.3V operation, the optimum value is generally around 0.044 +// for 5V operation, the optimum value is generally around 0.072 +// +const float powerCal_grid = 0.062; +const float powerCal_diverted = 0.062; + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. This mechanism can be used to offset any difference in +// phase delay between the voltage and current sensors. The algorithm interpolates +// between the most recent pair of voltage samples according to the phaseCal value. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value is used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. +// +// The calculation for real power is very insensitive to the value of phaseCal. +// When a "real power" calculation is used to determine how much surplus energy +// is available for diversion, a nominal value such as 1.0 is generally thought +// to be sufficient for this purpose. +// +const float phaseCal_grid = 1.0; +const float phaseCal_diverted = 1.0; + + +// For datalogging purposes, voltageCal has been included too. When running at +// 230 V AC, the range of ADC values will be similar to the actual range of volts, +// so the optimal value for this cal factor will be close to unity. +// +const float voltageCal = 1.0; + + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of this array is for the decimal point status. +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +volatile boolean EDD_isActive = false; // energy diversion detection +//volatile boolean EDD_isActive = true; // energy diversion detection + + +void setup() +{ + pinMode(outputForTrigger, OUTPUT); + digitalWrite (outputForTrigger, LOAD_OFF); // the external trigger is active low + + delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_RFdatalog_6.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, calibration values that are supplied in floating point + // form need to be rescaled. + // + phaseCal_grid_int = phaseCal_grid * 256; // for integer maths + phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail just before the energy bucket is updated at the start + // of each new mains cycle. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone's value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1): + capacityOfEnergyBucket_long = + (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + + IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; // may be useful + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled in a known way. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_diverted); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.println ("ADC mode: free-running"); + + // Set up the ADC to be free-running + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + configureParamsForSelectedOutputMode(); + Serial.println ("----"); + + convertTemperature(); // start initial temperature conversion + + Serial.print ("RF capability "); + +#ifdef RF_PRESENT + Serial.print ("IS present, freq = "); + if (freq == RF12_433MHZ) { Serial.println ("433 MHz"); } + if (freq == RF12_868MHZ) { Serial.println ("868 MHz"); } + rf12_initialize(nodeID, freq, networkGroup); // initialize RF +#else + Serial.println ("is NOT present"); +#endif + + convertTemperature(); // start initial temperature conversion +} + +/* None of the workload in loop() is time-critical. All the processing of + * ADC data is done within the ISR. + */ +void loop() +{ +// unsigned long timeNow = millis(); + static byte perSecondTimer = 0; +// + // The ISR provides a 50 Hz 'tick' which the main code is free to use. + if (newMainsCycle) + { + newMainsCycle = false; + perSecondTimer++; + + if(perSecondTimer >= CYCLES_PER_SECOND) + { + perSecondTimer = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // Clear the accumulators for diverted energy. These are the "genuine" + // accumulators that are used by ISR rather than the copies that are + // regularly made available for use by the main code. + // + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); // this timing is not critical so does not need to be in the ISR + } + } + + if (datalogEventPending) + { + datalogEventPending= false; + tx_data.powerAtSupplyPoint_Watts = copyOf_sumP_atSupplyPoint * powerCal_grid / copyOf_sampleSetsDuringThisDatalogPeriod; + tx_data.powerAtSupplyPoint_Watts *= -1; // to match the OEM convention (import is =ve; export is -ve) + tx_data.divertedEnergyTotal_Wh = copyOf_divertedEnergyTotal_Wh; + tx_data.divertedPower_Watts = copyOf_sumP_forDivertedPower * powerCal_diverted / copyOf_sampleSetsDuringThisDatalogPeriod; + tx_data.Vrms_times100 = (int)(100 * voltageCal * sqrt(copyOf_sum_Vsquared / copyOf_sampleSetsDuringThisDatalogPeriod)); + tx_data.temperature_times100 = readTemperature(); + +#ifdef RF_PRESENT + send_rf_data(); +#endif + + Serial.print("grid power "); Serial.print(tx_data.powerAtSupplyPoint_Watts); + Serial.print(", diverted energy (Wh) "); Serial.print(tx_data.divertedEnergyTotal_Wh); + Serial.print(", diverted power "); Serial.print(tx_data.divertedPower_Watts); + Serial.print(", Vrms "); Serial.print((float)tx_data.Vrms_times100 / 100); + Serial.print(", temperature "); Serial.print((float)tx_data.temperature_times100 / 100); + Serial.print(" [minSampleSets/MC "); Serial.print(copyOf_lowestNoOfSampleSetsPerMainsCycle); +// Serial.print(", #ofSampleSets "); Serial.print(copyOf_sampleSetsDuringThisDatalogPeriod); + Serial.println(']'); +// delay(POST_DATALOG_EVENT_DELAY_MILLIS); + convertTemperature(); // for use next time around + } + +/* + // occasional delays should not affect the operation of this revised code structure. + if (timeNow - timeAtLastDelay > 1000) + { + delay(100); + Serial.println("100ms delay"); + timeAtLastDelay = timeNow; + } +*/ +} + + +ISR(ADC_vect) +/* + * This Interrupt Service Routine looks after the acquisition and processing of + * raw samples from the ADC sub-processor. By means of various helper functions, all of + * the time-critical activities are processed within the ISR. The main code is notified + * by means of a flag when fresh copies of loggable data are available. + */ +{ + static unsigned char sample_index = 0; + int rawSample; + long sampleIminusDC; + long phaseShiftedSampleVminusDC; + long filtV_div4; + long filtI_div4; + long instP; + long inst_Vsquared; + + switch(sample_index) + { + case 0: + rawSample = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // the conversion for I_grid is already under way + sample_index++; // increment the control flag + // + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + sampleVminusDC_long = ((long)rawSample<<8) - DCoffset_V_long; + if(sampleVminusDC_long > 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + // + checkProgress(); // deals with aspects that only occur at particular stages of each mains cycle + // + // for the Vrms calculation (for datalogging only) + filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (now x4096, or 2^12) + inst_Vsquared = inst_Vsquared>>12; // scaling is now x1 (V_ADC x I_ADC) + sum_Vsquared += inst_Vsquared; // cumulative V^2 (V_ADC x I_ADC) + sampleSetsDuringThisDatalogPeriod++; + // + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter +// lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + sampleSetsDuringThisCycle++; // for real power calculations + refreshDisplay(); + break; + case 1: + rawSample = ADC; // store the ADC value (this one is for Grid Current) + ADMUX = 0x40 + voltageSensor; // the conversion for I_diverted is already under way + sample_index++; // increment the control flag + // + // remove most of the DC offset from the current sample (the precise value does not matter) + sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8; + // + // phase-shift the voltage waveform so that it aligns with the grid current waveform + phaseShiftedSampleVminusDC = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + // + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + // + sumP_forEnergyBucket+=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + sumP_atSupplyPoint +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + break; + case 2: + rawSample = ADC; // store the ADC value (this one is for Diverted Current) + ADMUX = 0x40 + currentSensor_grid; // the conversion for Voltage is already under way + sample_index = 0; // reset the control flag + // + // remove most of the DC offset from the current sample (the precise value does not matter) + sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8; + // + // phase-shift the voltage waveform so that it aligns with the diverted current waveform + phaseShiftedSampleVminusDC = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + // + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + // + sumP_forDivertedEnergy +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + sumP_forDivertedPower +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + break; + default: + sample_index = 0; // to prevent lockup (should never get here) + } +} + +/* ----------------------------------------------------------- + * Start of various helper functions which are used by the ISR + */ + +void checkProgress() +/* + * This routine is called by the ISR when each voltage sample becomes available. + * At the start of each new mains cycle, another helper function is called. + * All other processing is done within this function. + */ +{ + static enum loadStates nextStateOfLoad = LOAD_OFF; + + if (polarityConfirmed == POSITIVE) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + if (beyondStartUpPhase) + { + // The start of a new mains cycle, just after the +ve going zero-crossing point. + + // a simple routine for checking the performance of this new ISR structure + if (sampleSetsDuringThisCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisCycle; } + + processLatestContribution(); // for activities at the start of each new mains cycle + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000) + { + beyondStartUpPhase = true; + sumP_forEnergyBucket = 0; + sumP_atSupplyPoint = 0; + sumP_forDivertedEnergy = 0; + sumP_forDivertedPower = 0; + sampleSetsDuringThisCycle = 0; // not yet dealt with for this cycle + sampleSetsDuringThisDatalogPeriod = 0; + // can't say "Go!" here 'cos we're in an ISR! + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + // check to see whether the trigger device can now be reliably armed + // + if (sampleSetsDuringThisCycle == 5) // part way through the +ve half cycle + { + if (beyondStartUpPhase) + { + if (energyInBucket_long < lowerEnergyThreshold_long) { + // when below the lower threshold, always set the load to "off" + nextStateOfLoad = LOAD_OFF; } + else + if (energyInBucket_long > upperEnergyThreshold_long) { + // when above the upper threshold, always set the load to "off" + nextStateOfLoad = LOAD_ON; } + else { + } // leave the load's state unchanged (hysteresis) + + // set the Arduino's output pin accordingly + digitalWrite(outputForTrigger, nextStateOfLoad); + + // update the Energy Diversion Detector + if (nextStateOfLoad == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + } + } + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12); + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 230V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need for a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + +// checkOutputModeSelection(); // updates outputMode if the external switch is in use + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is negative +} // end of checkProgress() + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count > PERSISTENCE_FOR_POLARITY_CHANGE) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + + +void processLatestContribution() +/* + * This routine runs once per mains cycle. It forms part of the ISR. + */ +{ + newMainsCycle = true; // <-- a 50 Hz 'tick' for use by the main code + + // For the mechanism which controls the diversion of surplus power, the AVERAGE power + // at the 'grid' point during the previous mains cycle must be quantified. The first + // stage in this process is for the sum of all instantaneous power values to be divided + // by the number of sample sets that have contributed to its value. A similar operation + // is required for the diverted power data. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_for_energyBucket = sumP_forEnergyBucket / sampleSetsDuringThisCycle; + long realPower_diverted = sumP_forDivertedEnergy / sampleSetsDuringThisCycle; + // + // The per-mainsCycle variables can now be reset for ongoing use + sampleSetsDuringThisCycle = 0; + sumP_forEnergyBucket = 0; + sumP_forDivertedEnergy = 0; + + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Average power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variables below are CYCLES_PER_SECOND * (1/powerCal) times larger than + // their actual values in Joules. + // + long realEnergy_for_energyBucket = realPower_for_energyBucket; + long realEnergy_diverted = realPower_diverted; + + // The latest energy contribution from the grid connection point can now be added + // to the energy bucket which determines the state of the dump-load. + // + energyInBucket_long += realEnergy_for_energyBucket; + energyInBucket_long -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation + + // Apply max and min limits to the bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. To avoid the displayed + // value from creeping, any small contributions which are likely to be + // caused by noise are ignored. + // + if (realEnergy_diverted > antiCreepLimit_inIEUperMainsCycle) { + divertedEnergyRecent_IEU += realEnergy_diverted; } + + // Whole Watt-Hours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + /* At the end of each datalogging period, copies are made of the relevant variables + * for use by the main code. These variable are then reset for use during the next + * datalogging period. + */ + cycleCountForDatalogging ++; + if (cycleCountForDatalogging >= DATALOG_PERIOD_IN_MAINS_CYCLES ) + { + cycleCountForDatalogging = 0; + + copyOf_sumP_atSupplyPoint = sumP_atSupplyPoint; + copyOf_sumP_forDivertedPower = sumP_forDivertedPower; + copyOf_divertedEnergyTotal_Wh = divertedEnergyTotal_Wh; + copyOf_sum_Vsquared = sum_Vsquared; + copyOf_sampleSetsDuringThisDatalogPeriod = sampleSetsDuringThisDatalogPeriod; // (for diags only) + copyOf_lowestNoOfSampleSetsPerMainsCycle = lowestNoOfSampleSetsPerMainsCycle; // (for diags only) + + sumP_atSupplyPoint = 0; + sumP_forDivertedPower = 0; + sum_Vsquared = 0; + lowestNoOfSampleSetsPerMainsCycle = 999; + sampleSetsDuringThisDatalogPeriod = 0; + datalogEventPending = true; + } +} +/* End of helper functions which are used by the ISR + * ------------------------------------------------- + */ + +// this function changes the value of outputMode if the external switch is in use for this purpose +void checkOutputModeSelection() +{ + static byte count = 0; + int pinState; + // pinState = digitalRead(outputModeSelectorPin); <- pin re-allocated for Dallas sensor + if (pinState != outputMode) + { + count++; + } + if (count >= 20) + { + count = 0; + outputMode = (enum outputModes)pinState; // change the global variable + Serial.print ("outputMode selection changed to "); + if (outputMode == NORMAL) { + Serial.println ( "normal"); } + else { + Serial.println ( "anti-flicker"); } + + configureParamsForSelectedOutputMode(); + } +} + + + +void configureParamsForSelectedOutputMode() +/* + * retained for compatibility with previous versions + */ +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + upperEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" lowerEnergyThreshold_long = "); + Serial.println(lowerEnergyThreshold_long); + Serial.print(" upperEnergyThreshold_long = "); + Serial.println(upperEnergyThreshold_long); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +} + + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + +void convertTemperature() +{ + oneWire.reset(); + oneWire.write(SKIP_ROM); + oneWire.write(CONVERT_TEMPERATURE); +} + +int readTemperature() +{ + byte buf[9]; + int result; + + oneWire.reset(); + oneWire.write(SKIP_ROM); + oneWire.write(READ_SCRATCHPAD); + for(int i=0; i<9; i++) buf[i]=oneWire.read(); + if(oneWire.crc8(buf,8)==buf[8]) + { + result=(buf[1]<<8)|buf[0]; + // result is temperature x16, multiply by 6.25 to convert to temperature x100 + result=(result*6)+(result>>2); + } + else result=BAD_TEMPERATURE; + return result; +} + + +#ifdef RF_PRESENT +void send_rf_data() +// +// To avoid disturbance to the sampling process, the RFM12B needs to remain in its +// active state rather than being periodically put to sleep. +{ + // check whether it's ready to send, and an exit route if it gets stuck + int i = 0; + while (!rf12_canSend() && i<10) + { + rf12_recvDone(); + i++; + } + rf12_sendNow(0, &tx_data, sizeof tx_data); +} +#endif + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_7.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_7.ino new file mode 100644 index 0000000..f642a57 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_7.ino @@ -0,0 +1,1190 @@ +/* Mk2_RFdatalog_7.ino + * + * This sketch is for diverting suplus PV power to a dump load using a triac + * or Solid State Relay. Routine datalogging is also supported using the + * on-board RF module (either RFM12B or RF69). + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The integral voltage sensor is fed from one of the secondary coils of the + * transformer. Current is measured via Current Transformers at the + * CT1 and CT1 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. When the RFM12B module is + * in use, the display can only be used in conjunction with an extra pair of + * logic chips. These are ICs 3 and 4, which reduce the number of processor pins + * that are needed to drive the display. + * + * This sketch is based on the Mk2i PV Router code that I have posted in on the + * OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * September 2014: renamed as Mk2_RFdatalog_3, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - tidier initialisation of display logic in setup(); + * + * December 2014, upgraded to Mk2_RFdatalog_4: + * This sketch has been restructured in order to make better use of the ISR. All of + * the time-critical code is now contained within the ISR and its helper functions. + * Values for datalogging are transferred to the main code using a flag-based handshake + * mechanism. The diversion of surplus power can no longer be affected by slower + * activities which may be running in the main code such as Serial statements and RF. + * Temperature sensing is supported by re-allocating the "mode" port for this + * purpose. A pullup resistor (4K7 or similar) is required for the Dallas sensor. The + * output mode, i.e. NORMAL or ANTI_FLICKER, is now set at compile time. + * Also: + * - The ADC is now in free-running mode, at ~104 us per conversion. + * - a persistence check has been added for zero-crossing detection (polarityConfirmed) + * - a lowestNoOfSampleSetsPerMainsCycle check has been added, to detect any disturbances + * - Vrms has been added to the datalog payload (as Vrms x 100) + * - temperature has been added to the datalog payload (as degrees C x 100) + * - the phaseCal mechanism has been reinstated + * + * January 2016: renamed as Mk2_RFdatalog_4a, with a minor change in the ISR to reinstate + * the phaseCal calculation. Previously, this feature was having no effect because + * two assignment lines were in the wrong order. When measuring "real power", which + * is what this application does, the phaseCal refinement has very little effect even + * when correctly implemented, as it now is. + * Support for the RF69 RF module has also been added. + * + * January 2016: updated to Mk2_RFdatalog_4b: + * The variables to store copies of ADC results for use by the main code are now declared + * as "volatile" to remove any possibility of incorrect operation due to optimisation + * by the compiler. + * + * February 2016: updated to Mk2_RFdatalog_5, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * March 2016: updated to Mk2_RFdatalog_5a, with this change: + * - RF capability made switchable so that the code will continue to run + * when an RF module is not fitted. Dataloging can then take place + * via the Serial port. + * + * November 2020: updated to Mk2_RFdatalog_6, with these changes: + * - the parameter cycleCountForDatalogging is now an "int" rather that a "byte". This + * allows the datalogging period to be extended beyond 5 seconds without the counter + * running out of range. + * - diverted energy, as monitored by CT2, is now reported as an average power as well as + * the cumulative energy total for the current day. + * + * July 2022: updated to Mk2_RFdatalog_7, with this change: + * - the datalogging accumulators for grid power, diverted power and Vsquared have been rescaled + * to 1/16 of their previous values to avoid the risk of overflowing during a 10-second + * datalogging period. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include +#include + +#define RF_PRESENT // <- this line should be commented out if the RFM12B module is not present + +#ifdef RF_PRESENT +#define RF69_COMPAT 0 // for the RFM12B +// #define RF69_COMPAT 1 // for the RF69 +#include +#endif + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// ----------------------------------------------------- +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define WORKING_RANGE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// -------------------------- +// Dallas DS18B20 commands +#define SKIP_ROM 0xcc +#define CONVERT_TEMPERATURE 0x44 +#define READ_SCRATCHPAD 0xbe +#define BAD_TEMPERATURE 30000 // this value (300C) is sent if no sensor is present + +// ---------------- +// general literals +#define DATALOG_PERIOD_IN_MAINS_CYCLES 500 +// #define POST_DATALOG_EVENT_DELAY_MILLIS 40 +#define ANTI_CREEP_LIMIT 0 // <- to prevent the diverted energy total from 'creeping' + // in Joules per mains cycle (has no effect when set to 0) + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +// ------------------------------- +// definitions of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum loadStates {LOAD_ON, LOAD_OFF}; // the external trigger device is active low +enum outputModes {ANTI_FLICKER, NORMAL}; // retained for compatibility with previous versions. + +// ---- Output mode selection ----- +enum outputModes outputMode = ANTI_FLICKER; // <- needs to be set here unless an +//enum outputModes outputMode = NORMAL; // external switch is in use + +/* -------------------------------------- + * RF configuration (for the RFM12B module) + * frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ + */ +#ifdef RF_PRESENT +#define freq RF12_433MHZ + +const int nodeID = 10; // RFM12B node ID +const int networkGroup = 210; // wireless network group - needs to be same for all nodes +const int UNO = 1; // for when the processor contains the UNO bootloader. +#endif + +typedef struct { + int powerAtSupplyPoint_Watts; // import = +ve, to match OEM convention + int divertedEnergyTotal_Wh; // always positive + int divertedPower_Watts; // always positive + int Vrms_times100; + int temperature_times100; +} Tx_struct; +Tx_struct tx_data; + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +// D2 is for the RFM12B +const byte tempSensorPin = 3; // <-- the "mode" port +const byte outputForTrigger = 4; +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +// D10 is for the RFM12B +// D11 is for the RFM12B +// D12 is for the RFM12B +// D13 is for the RFM12B + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + +const byte delayBeforeSerialStarts = 5; // in seconds, to allow Serial window to be opened +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +/* ------------------------------------------------------------------------------------- + * Global variables that are used in multiple blocks so cannot be static. + * For integer maths, many variables need to be 'long' + */ +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long energyInBucket_long = 0; // in Integer Energy Units (for controlling the dump-load) +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long lowerEnergyThreshold_long; // for turning load off +long upperEnergyThreshold_long; // for turning load on +int phaseCal_grid_int; // to avoid the need for floating-point maths +int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF + +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; +float IEUtoJoulesConversion_CT1; + +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- not wise to exceeed 0.4 + +long sumP_forEnergyBucket; // for per-cycle summation of 'real power' +long sumP_forDivertedEnergy; // for per-cycle summation of diverted energy +long sumP_atSupplyPoint; // for summation of 'real power' values during datalog period +long sumP_forDivertedPower; // for summation of diverted power values during datalog period +long sum_Vsquared; // for summation of V^2 values during datalog period +int sampleSetsDuringThisCycle; // for counting the sample sets during each mains cycle +long sampleSetsDuringThisDatalogPeriod; // for counting the sample sets during each datalogging period + +long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) +long lastSampleVminusDC_long; // for the phaseCal algorithm +int cycleCountForDatalogging = 0; +long sampleVminusDC_long; +long requiredExportPerMainsCycle_inIEU; + +// for interaction between the main code and the ISR +volatile boolean datalogEventPending = false; +volatile boolean newMainsCycle = false; +volatile long copyOf_sumP_atSupplyPoint; +volatile long copyOf_sumP_forDivertedPower; +volatile long copyOf_sum_Vsquared; +volatile long copyOf_divertedEnergyTotal_Wh; +volatile int copyOf_lowestNoOfSampleSetsPerMainsCycle; +volatile long copyOf_sampleSetsDuringThisDatalogPeriod; + +// For temperature sensing +OneWire oneWire(tempSensorPin); +int tempTimes100; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define PERSISTENCE_FOR_POLARITY_CHANGE 2 +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; // for zero-crossing detection +enum polarities polarityConfirmedOfLastSampleV; // for zero-crossing detection + +// For a mechanism to check the integrity of this code structure +int lowestNoOfSampleSetsPerMainsCycle; +unsigned long timeAtLastDelay; + + +// Calibration values (not important for the Router's basic operation) +//------------------- +// For accurate calculation of real power/energy, two calibration values are +// used: powerCal and phaseCal. With most hardware, the default values are +// likely to work fine without need for change. A full explanation of each +// of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool_2chan.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. Any pre-built system that I supply will have been +// checked with this tool to ensure that the input sensors are working correctly. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 230V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 230V AC has a peak-to-peak amplitude of 651V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +// for 3.3V operation, the optimum value is generally around 0.044 +// for 5V operation, the optimum value is generally around 0.072 +// +const float powerCal_grid = 0.062; +const float powerCal_diverted = 0.062; + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. This mechanism can be used to offset any difference in +// phase delay between the voltage and current sensors. The algorithm interpolates +// between the most recent pair of voltage samples according to the phaseCal value. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value is used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. +// +// The calculation for real power is very insensitive to the value of phaseCal. +// When a "real power" calculation is used to determine how much surplus energy +// is available for diversion, a nominal value such as 1.0 is generally thought +// to be sufficient for this purpose. +// +const float phaseCal_grid = 1.0; +const float phaseCal_diverted = 1.0; + + +// For datalogging purposes, voltageCal has been included too. When running at +// 230 V AC, the range of ADC values will be similar to the actual range of volts, +// so the optimal value for this cal factor will be close to unity. +// +const float voltageCal = 1.0; + + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of this array is for the decimal point status. +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +volatile boolean EDD_isActive = false; // energy diversion detection +//volatile boolean EDD_isActive = true; // energy diversion detection + + +void setup() +{ + pinMode(outputForTrigger, OUTPUT); + digitalWrite (outputForTrigger, LOAD_OFF); // the external trigger is active low + + delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_RFdatalog_7.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, calibration values that are supplied in floating point + // form need to be rescaled. + // + phaseCal_grid_int = phaseCal_grid * 256; // for integer maths + phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail just before the energy bucket is updated at the start + // of each new mains cycle. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone's value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1): + capacityOfEnergyBucket_long = + (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + + IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; // may be useful + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled in a known way. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_diverted); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.println ("ADC mode: free-running"); + + // Set up the ADC to be free-running + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + configureParamsForSelectedOutputMode(); + Serial.println ("----"); + + convertTemperature(); // start initial temperature conversion + + Serial.print ("RF capability "); + +#ifdef RF_PRESENT + Serial.print ("IS present, freq = "); + if (freq == RF12_433MHZ) { Serial.println ("433 MHz"); } + if (freq == RF12_868MHZ) { Serial.println ("868 MHz"); } + rf12_initialize(nodeID, freq, networkGroup); // initialize RF +#else + Serial.println ("is NOT present"); +#endif + + convertTemperature(); // start initial temperature conversion +} + +/* None of the workload in loop() is time-critical. All the processing of + * ADC data is done within the ISR. + */ +void loop() +{ +// unsigned long timeNow = millis(); + static byte perSecondTimer = 0; +// + // The ISR provides a 50 Hz 'tick' which the main code is free to use. + if (newMainsCycle) + { + newMainsCycle = false; + perSecondTimer++; + + if(perSecondTimer >= CYCLES_PER_SECOND) + { + perSecondTimer = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // Clear the accumulators for diverted energy. These are the "genuine" + // accumulators that are used by ISR rather than the copies that are + // regularly made available for use by the main code. + // + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); // this timing is not critical so does not need to be in the ISR + } + } + + if (datalogEventPending) + { + datalogEventPending= false; + // To provide sufficient range for a dataloging period of 10 seconds, the accumulators for grid power + // and diverted power are now scaled at 1/16 of their previous V_ADC * I_ADC values. Hence the * 16 factor + // that appears below. + // Similarly, the accumulator for Vsquared is now scaled at 1/16 of its previous V_ADC * V_ADC value. + // Hence the * 4 factor that appears below after the sqrt() operation. + // + tx_data.powerAtSupplyPoint_Watts = + copyOf_sumP_atSupplyPoint * powerCal_grid / copyOf_sampleSetsDuringThisDatalogPeriod * 16; + tx_data.powerAtSupplyPoint_Watts *= -1; // to match the OEM convention (import is =ve; export is -ve) + tx_data.divertedEnergyTotal_Wh = copyOf_divertedEnergyTotal_Wh; + tx_data.divertedPower_Watts = + copyOf_sumP_forDivertedPower * powerCal_diverted / copyOf_sampleSetsDuringThisDatalogPeriod * 16; + tx_data.Vrms_times100 = + (int)(100 * voltageCal * sqrt(copyOf_sum_Vsquared / copyOf_sampleSetsDuringThisDatalogPeriod) * 4); + tx_data.temperature_times100 = readTemperature(); + +#ifdef RF_PRESENT + send_rf_data(); +#endif + + Serial.print("grid power "); Serial.print(tx_data.powerAtSupplyPoint_Watts); + Serial.print(", diverted energy (Wh) "); Serial.print(tx_data.divertedEnergyTotal_Wh); + Serial.print(", diverted power "); Serial.print(tx_data.divertedPower_Watts); + Serial.print(", Vrms "); Serial.print((float)tx_data.Vrms_times100 / 100); + Serial.print(", temperature "); Serial.print((float)tx_data.temperature_times100 / 100); + Serial.print(" [minSampleSets/MC "); Serial.print(copyOf_lowestNoOfSampleSetsPerMainsCycle); +// Serial.print(", #ofSampleSets "); Serial.print(copyOf_sampleSetsDuringThisDatalogPeriod); + Serial.println(']'); +// delay(POST_DATALOG_EVENT_DELAY_MILLIS); + convertTemperature(); // for use next time around + } + +/* + // occasional delays should not affect the operation of this revised code structure. + if (timeNow - timeAtLastDelay > 1000) + { + delay(100); + Serial.println("100ms delay"); + timeAtLastDelay = timeNow; + } +*/ +} + + +ISR(ADC_vect) +/* + * This Interrupt Service Routine looks after the acquisition and processing of + * raw samples from the ADC sub-processor. By means of various helper functions, all of + * the time-critical activities are processed within the ISR. The main code is notified + * by means of a flag when fresh copies of loggable data are available. + */ +{ + static unsigned char sample_index = 0; + int rawSample; + long sampleIminusDC; + long phaseShiftedSampleVminusDC; + long filtV_div4; + long filtI_div4; + long instP; + long inst_Vsquared; + + switch(sample_index) + { + case 0: + rawSample = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // the conversion for I_grid is already under way + sample_index++; // increment the control flag + // + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + sampleVminusDC_long = ((long)rawSample<<8) - DCoffset_V_long; + if(sampleVminusDC_long > 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + // + checkProgress(); // deals with aspects that only occur at particular stages of each mains cycle + // + // for the Vrms calculation (for datalogging only) + filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (x64, or 2^6) + inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (x4096, or 2^12) +// inst_Vsquared = inst_Vsquared>>12; // 20-bits (x1), not enough range :-( + inst_Vsquared = inst_Vsquared>>16; // 16-bits (x1/16, or 2^-4) for more datalog range + sum_Vsquared += inst_Vsquared; // scaling is x1/16 + sampleSetsDuringThisDatalogPeriod++; + // + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter +// lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + sampleSetsDuringThisCycle++; // for real power calculations + refreshDisplay(); + break; + case 1: + rawSample = ADC; // store the ADC value (this one is for Grid Current) + ADMUX = 0x40 + voltageSensor; // the conversion for I_diverted is already under way + sample_index++; // increment the control flag + // + // remove most of the DC offset from the current sample (the precise value does not matter) + sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8; + // + // phase-shift the voltage waveform so that it aligns with the grid current waveform + phaseShiftedSampleVminusDC = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + // + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (x64, or 2^6) + filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (x4096, or 2^12) + instP = instP>>12; // reduce to 20-bits (x1) + sumP_forEnergyBucket+=instP; // scaling is x1 + // + instP = instP>>4; // reduce to 16-bits (x1/16, or 2^-4) for more datalog range + sumP_atSupplyPoint +=instP; // scaling is x1/16 + break; + case 2: + rawSample = ADC; // store the ADC value (this one is for Diverted Current) + ADMUX = 0x40 + currentSensor_grid; // the conversion for Voltage is already under way + sample_index = 0; // reset the control flag + // + // remove most of the DC offset from the current sample (the precise value does not matter) + sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8; + // + // phase-shift the voltage waveform so that it aligns with the diverted current waveform + phaseShiftedSampleVminusDC = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + // + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (x64, or 2^6) + filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (x4096, or 2^12) + instP = instP>>12; // reduce to 20-bits (x1) + sumP_forDivertedEnergy +=instP; // scaling is x1 + // + instP = instP>>4; // reduce to 16-bits (x1/16, or 2^-4) for more datalog range + sumP_forDivertedPower +=instP; // scaling is x1/16 + break; + default: + sample_index = 0; // to prevent lockup (should never get here) + } +} + +/* ----------------------------------------------------------- + * Start of various helper functions which are used by the ISR + */ + +void checkProgress() +/* + * This routine is called by the ISR when each voltage sample becomes available. + * At the start of each new mains cycle, another helper function is called. + * All other processing is done within this function. + */ +{ + static enum loadStates nextStateOfLoad = LOAD_OFF; + + if (polarityConfirmed == POSITIVE) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + if (beyondStartUpPhase) + { + // The start of a new mains cycle, just after the +ve going zero-crossing point. + + // a simple routine for checking the performance of this new ISR structure + if (sampleSetsDuringThisCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisCycle; } + + processLatestContribution(); // for activities at the start of each new mains cycle + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000) + { + beyondStartUpPhase = true; + sumP_forEnergyBucket = 0; + sumP_atSupplyPoint = 0; + sumP_forDivertedEnergy = 0; + sumP_forDivertedPower = 0; + sampleSetsDuringThisCycle = 0; // not yet dealt with for this cycle + sampleSetsDuringThisDatalogPeriod = 0; + // can't say "Go!" here 'cos we're in an ISR! + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + // check to see whether the trigger device can now be reliably armed + // + if (sampleSetsDuringThisCycle == 5) // part way through the +ve half cycle + { + if (beyondStartUpPhase) + { + if (energyInBucket_long < lowerEnergyThreshold_long) { + // when below the lower threshold, always set the load to "off" + nextStateOfLoad = LOAD_OFF; } + else + if (energyInBucket_long > upperEnergyThreshold_long) { + // when above the upper threshold, always set the load to "off" + nextStateOfLoad = LOAD_ON; } + else { + } // leave the load's state unchanged (hysteresis) + + // set the Arduino's output pin accordingly + digitalWrite(outputForTrigger, nextStateOfLoad); + + // update the Energy Diversion Detector + if (nextStateOfLoad == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + } + } + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12); + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 230V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need for a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + +// checkOutputModeSelection(); // updates outputMode if the external switch is in use + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is negative +} // end of checkProgress() + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count > PERSISTENCE_FOR_POLARITY_CHANGE) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + + +void processLatestContribution() +/* + * This routine runs once per mains cycle. It forms part of the ISR. + */ +{ + newMainsCycle = true; // <-- a 50 Hz 'tick' for use by the main code + + // For the mechanism which controls the diversion of surplus power, the AVERAGE power + // at the 'grid' point during the previous mains cycle must be quantified. The first + // stage in this process is for the sum of all instantaneous power values to be divided + // by the number of sample sets that have contributed to its value. A similar operation + // is required for the diverted power data. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_for_energyBucket = sumP_forEnergyBucket / sampleSetsDuringThisCycle; + long realPower_diverted = sumP_forDivertedEnergy / sampleSetsDuringThisCycle; + // + // The per-mainsCycle variables can now be reset for ongoing use + sampleSetsDuringThisCycle = 0; + sumP_forEnergyBucket = 0; + sumP_forDivertedEnergy = 0; + + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Average power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variables below are CYCLES_PER_SECOND * (1/powerCal) times larger than + // their actual values in Joules. + // + long realEnergy_for_energyBucket = realPower_for_energyBucket; + long realEnergy_diverted = realPower_diverted; + + // The latest energy contribution from the grid connection point can now be added + // to the energy bucket which determines the state of the dump-load. + // + energyInBucket_long += realEnergy_for_energyBucket; + energyInBucket_long -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation + + // Apply max and min limits to the bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. To avoid the displayed + // value from creeping, any small contributions which are likely to be + // caused by noise are ignored. + // + if (realEnergy_diverted > antiCreepLimit_inIEUperMainsCycle) { + divertedEnergyRecent_IEU += realEnergy_diverted; } + + // Whole Watt-Hours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + /* At the end of each datalogging period, copies are made of the relevant variables + * for use by the main code. These variable are then reset for use during the next + * datalogging period. + */ + cycleCountForDatalogging ++; + if (cycleCountForDatalogging >= DATALOG_PERIOD_IN_MAINS_CYCLES ) + { + cycleCountForDatalogging = 0; + + copyOf_sumP_atSupplyPoint = sumP_atSupplyPoint; + copyOf_sumP_forDivertedPower = sumP_forDivertedPower; + copyOf_divertedEnergyTotal_Wh = divertedEnergyTotal_Wh; + copyOf_sum_Vsquared = sum_Vsquared; + copyOf_sampleSetsDuringThisDatalogPeriod = sampleSetsDuringThisDatalogPeriod; // (for diags only) + copyOf_lowestNoOfSampleSetsPerMainsCycle = lowestNoOfSampleSetsPerMainsCycle; // (for diags only) + + sumP_atSupplyPoint = 0; + sumP_forDivertedPower = 0; + sum_Vsquared = 0; + lowestNoOfSampleSetsPerMainsCycle = 999; + sampleSetsDuringThisDatalogPeriod = 0; + datalogEventPending = true; + } +} +/* End of helper functions which are used by the ISR + * ------------------------------------------------- + */ + +// this function changes the value of outputMode if the external switch is in use for this purpose +void checkOutputModeSelection() +{ + static byte count = 0; + int pinState; + // pinState = digitalRead(outputModeSelectorPin); <- pin re-allocated for Dallas sensor + if (pinState != outputMode) + { + count++; + } + if (count >= 20) + { + count = 0; + outputMode = (enum outputModes)pinState; // change the global variable + Serial.print ("outputMode selection changed to "); + if (outputMode == NORMAL) { + Serial.println ( "normal"); } + else { + Serial.println ( "anti-flicker"); } + + configureParamsForSelectedOutputMode(); + } +} + + + +void configureParamsForSelectedOutputMode() +/* + * retained for compatibility with previous versions + */ +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + upperEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" lowerEnergyThreshold_long = "); + Serial.println(lowerEnergyThreshold_long); + Serial.print(" upperEnergyThreshold_long = "); + Serial.println(upperEnergyThreshold_long); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +} + + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + +void convertTemperature() +{ + oneWire.reset(); + oneWire.write(SKIP_ROM); + oneWire.write(CONVERT_TEMPERATURE); +} + +int readTemperature() +{ + byte buf[9]; + int result; + + oneWire.reset(); + oneWire.write(SKIP_ROM); + oneWire.write(READ_SCRATCHPAD); + for(int i=0; i<9; i++) buf[i]=oneWire.read(); + if(oneWire.crc8(buf,8)==buf[8]) + { + result=(buf[1]<<8)|buf[0]; + // result is temperature x16, multiply by 6.25 to convert to temperature x100 + result=(result*6)+(result>>2); + } + else result=BAD_TEMPERATURE; + return result; +} + + +#ifdef RF_PRESENT +void send_rf_data() +// +// To avoid disturbance to the sampling process, the RFM12B needs to remain in its +// active state rather than being periodically put to sleep. +{ + // check whether it's ready to send, and an exit route if it gets stuck + int i = 0; + while (!rf12_canSend() && i<10) + { + rf12_recvDone(); + i++; + } + rf12_sendNow(0, &tx_data, sizeof tx_data); +} +#endif + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_multiLoad_1.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_multiLoad_1.ino new file mode 100644 index 0000000..d926631 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_multiLoad_1.ino @@ -0,0 +1,1400 @@ +/* Mk2_RFdatalog_multiLoad_1.ino is based on Mk2_RFdatalog_5a + * + * This sketch is for diverting surplus PV power to one or two dump loads using + * triac-based output stages or Solid State Relays. Routine datalogging is + * avalable if a suitable RF module is fitted (either RFM12B or RF69). A 4-digit display + * showing the Diverted Energy each day is also supported. + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The integral voltage sensor is fed from one of the secondary coils of the + * transformer. Current is measured via Current Transformers at the + * CT1 and CT2 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. When the RF module is + * in use, the display can only be used in conjunction with an extra pair of + * logic chips. These are ICs 3 and 4, which reduce the number of processor pins + * that are needed to drive the display. + * + * This sketch is based on the Mk2i PV Router code that I have posted in on the + * OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * September 2014: renamed as Mk2_RFdatalog_3, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - tidier initialisation of display logic in setup(); + * + * December 2014, upgraded to Mk2_RFdatalog_4: + * This sketch has been restructured in order to make better use of the ISR. All of + * the time-critical code is now contained within the ISR and its helper functions. + * Values for datalogging are transferred to the main code using a flag-based handshake + * mechanism. The diversion of surplus power can no longer be affected by slower + * activities which may be running in the main code such as Serial statements and RF. + * Temperature sensing is supported by re-allocating the "mode" port for this + * purpose. A pullup resistor (4K7 or similar) is required for the Dallas sensor. The + * output mode, i.e. NORMAL or ANTI_FLICKER, is now set at compile time. + * Also: + * - The ADC is now in free-running mode, at ~104 us per conversion. + * - a persistence check has been added for zero-crossing detection (polarityConfirmed) + * - a lowestNoOfSampleSetsPerMainsCycle check has been added, to detect any disturbances + * - Vrms has been added to the datalog payload (as Vrms x 100) + * - temperature has been added to the datalog payload (as degrees C x 100) + * - the phaseCal mechanism has been reinstated + * + * January 2016: renamed as Mk2_RFdatalog_4a, with a minor change in the ISR to reinstate + * the phaseCal calculation. Previously, this feature was having no effect because + * two assignment lines were in the wrong order. When measuring "real power", which + * is what this application does, the phaseCal refinement has very little effect even + * when correctly implemented, as it now is. + * Support for the RF69 RF module has also been added. + * + * January 2016: updated to Mk2_RFdatalog_4b: + * The variables to store copies of ADC results for use by the main code are now declared + * as "volatile" to remove any possibility of incorrect operation due to optimisation + * by the compiler. + * + * February 2016: updated to Mk2_RFdatalog_5, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * March 2016: updated to Mk2_RFdatalog_5a, with this change: + * - RF capability made switchable so that the code will continue to run + * when an RF module is not fitted. Dataloging can then take place + * via the Serial port. + * + * October 2017: updated to Mk2_RFdatalog_multiLoad_1, with these changes: + * - temperature sensing commented out(formally supported via D3 at the "mode" port") + * - support for a second load added (vcontrolled via D3 at the "mode" port") + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include +// #include // for temperature sensing + +#define RF_PRESENT // <- this line must be commented out if the RFM12B module is not present + +#ifdef RF_PRESENT +#define RF69_COMPAT 0 // for the RFM12B +// #define RF69_COMPAT 1 // for the RF69 +#include +#endif + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// ----------------------------------------------------- +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define WORKING_RANGE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +/* +// -------------------------- +// Dallas DS18B20 commands +#define SKIP_ROM 0xcc +#define CONVERT_TEMPERATURE 0x44 +#define READ_SCRATCHPAD 0xbe +#define BAD_TEMPERATURE 30000 // this value (300C) is sent if no sensor is present +*/ + +// ---------------- +// general literals +#define DATALOG_PERIOD_IN_MAINS_CYCLES 250 +#define ANTI_CREEP_LIMIT 0 // <- to prevent the diverted energy total from 'creeping' + // in Joules per mains cycle (has no effect when set to 0) + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +const byte noOfDumploads = 2; + +// ------------------------------- +// definitions of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum outputModes {ANTI_FLICKER, NORMAL}; // retained for compatibility with previous versions. +enum loadPriorityModes {LOAD_1_HAS_PRIORITY, LOAD_0_HAS_PRIORITY}; + +enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are active low +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +// ---- Output mode selection ----- +// enum outputModes outputMode = ANTI_FLICKER; // <- needs to be set here unless an +enum outputModes outputMode = NORMAL; // external switch is in use + +enum loadPriorityModes loadPriorityMode = LOAD_0_HAS_PRIORITY; // <- needs to be set here unless an +// enum loadPriorityModes loadPriorityMode = LOAD_1_HAS_PRIORITY; // <- external switch is in use + +/* -------------------------------------- + * RF configuration (for the RFM12B module) + * frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ + */ +#ifdef RF_PRESENT +#define freq RF12_868MHZ + +const int nodeID = 10; // RFM12B node ID +const int networkGroup = 210; // wireless network group - needs to be same for all nodes +const int UNO = 1; // for when the processor contains the UNO bootloader. +#endif + +typedef struct { + int powerAtSupplyPoint_Watts; // import = +ve, to match OEM convention + int divertedEnergyTotal_Wh; // always positive + int Vrms_times100; +// int temperature_times100; +} Tx_struct; +Tx_struct tx_data; + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +// D2 is for the RFM12B +//const byte tempSensorPin = 3; // <-- the "mode" port +const byte physicalLoad_1_pin = 3; // <-- the "mode" port is active high +const byte physicalLoad_0_pin = 4; // <-- the "trigger" port is active low +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +// D10 is for the RFM12B +// D11 is for the RFM12B +// D12 is for the RFM12B +// D13 is for the RFM12B + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + +const byte delayBeforeSerialStarts = 2; // in seconds, to allow Serial window to be opened +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +/* ------------------------------------------------------------------------------------- + * Global variables that are used in multiple blocks so cannot be static. + * For integer maths, many variables need to be 'long' + */ +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long energyInBucket_long = 0; // in Integer Energy Units (for controlling the dump-load) +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long midPointOfEnergyBucket_long; // used for 'normal' and single-threshold 'AF' logic +int phaseCal_grid_int; // to avoid the need for floating-point maths +int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF + +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; +float IEUtoJoulesConversion_CT1; + +long lowerThreshold_default; +long lowerEnergyThreshold; +long upperThreshold_default; +long upperEnergyThreshold; + +boolean recentTransition; +byte postTransitionCount; +#define POST_TRANSITION_MAX_COUNT 3 // <-- allows each transition to take effect +byte activeLoad = 0; + +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- not wise to exceeed 0.4 + +long sumP_forEnergyBucket; // for per-cycle summation of 'real power' +long sumP_diverted; // for per-cycle summation of diverted power +long sumP_atSupplyPoint; // for summation of 'real power' values during datalog period +long sum_Vsquared; // for summation of V^2 values during datalog period +int sampleSetsDuringThisCycle; // for counting the sample sets during each mains cycle +long sampleSetsDuringThisDatalogPeriod; // for counting the sample sets during each datalogging period + +long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) +long lastSampleVminusDC_long; // for the phaseCal algorithm +byte cycleCountForDatalogging = 0; +long sampleVminusDC_long; +long requiredExportPerMainsCycle_inIEU; + +// for interaction between the main code and the ISR +volatile boolean datalogEventPending = false; +volatile boolean newMainsCycle = false; +volatile long copyOf_sumP_atSupplyPoint; +volatile long copyOf_sum_Vsquared; +volatile long copyOf_divertedEnergyTotal_Wh; +volatile int copyOf_lowestNoOfSampleSetsPerMainsCycle; +volatile long copyOf_sampleSetsDuringThisDatalogPeriod; + +/* + * // For temperature sensing +OneWire oneWire(tempSensorPin); +int tempTimes100; +*/ + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define PERSISTENCE_FOR_POLARITY_CHANGE 2 +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; // for zero-crossing detection +enum polarities polarityConfirmedOfLastSampleV; // for zero-crossing detection + +// For a mechanism to check the integrity of this code structure +int lowestNoOfSampleSetsPerMainsCycle; +unsigned long timeAtLastDelay; + + +// Calibration values (not important for the Router's basic operation) +//------------------- +// For accurate calculation of real power/energy, two calibration values are +// used: powerCal and phaseCal. With most hardware, the default values are +// likely to work fine without need for change. A full explanation of each +// of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool_2chan.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. Any pre-built system that I supply will have been +// checked with this tool to ensure that the input sensors are working correctly. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 230V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 230V AC has a peak-to-peak amplitude of 651V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +// for 3.3V operation, the optimum value is generally around 0.044 +// for 5V operation, the optimum value is generally around 0.072 +// +const float powerCal_grid = 0.072; +const float powerCal_diverted = 0.073; + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. This mechanism can be used to offset any difference in +// phase delay between the voltage and current sensors. The algorithm interpolates +// between the most recent pair of voltage samples according to the phaseCal value. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value is used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. +// +// The calculation for real power is very insensitive to the value of phaseCal. +// When a "real power" calculation is used to determine how much surplus energy +// is available for diversion, a nominal value such as 1.0 is generally thought +// to be sufficient for this purpose. +// +const float phaseCal_grid = 1.0; +const float phaseCal_diverted = 1.0; + + +// For datalogging purposes, voltageCal has been included too. When running at +// 230 V AC, the range of ADC values will be similar to the actual range of volts, +// so the optimal value for this cal factor will be close to unity. +// +const float voltageCal = 1.0; + + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of this array is for the decimal point status. +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +volatile boolean EDD_isActive = false; // energy diversion detection +//volatile boolean EDD_isActive = true; // (for test purposes only) + + +void setup() +{ + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for the local dump-load + pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for an additional load + + for(int i = 0; i< noOfDumploads; i++) + { + logicalLoadState[i] = LOAD_OFF; + physicalLoadState[i] = LOAD_OFF; + } + + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // the local load is active low. + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // additional loads are active high. + +/* + pinMode(loadPrioritySelectorPin, INPUT); // this pin is tracked to the "mode" connector + digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and + loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority +*/ + + delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_RFdatalog_5a.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, calibration values that are supplied in floating point + // form need to be rescaled. + // + phaseCal_grid_int = phaseCal_grid * 256; // for integer maths + phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail just before the energy bucket is updated at the start + // of each new mains cycle. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone's value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1): + capacityOfEnergyBucket_long = + (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + midPointOfEnergyBucket_long = capacityOfEnergyBucket_long / 2; + IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; // may be useful + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled in a known way. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_diverted); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.println ("ADC mode: free-running"); + + // Set up the ADC to be free-running + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + configureParamsForSelectedOutputMode(); + Serial.println ("----"); + +// convertTemperature(); // start initial temperature conversion + + Serial.print ("RF capability "); + +#ifdef RF_PRESENT + Serial.print ("IS present, freq = "); + if (freq == RF12_433MHZ) { Serial.println ("433 MHz"); } + if (freq == RF12_868MHZ) { Serial.println ("868 MHz"); } + rf12_initialize(nodeID, freq, networkGroup); // initialize RF +#else + Serial.println ("is NOT present"); +#endif + +// convertTemperature(); // start initial temperature conversion +} + +/* None of the workload in loop() is time-critical. All the processing of + * ADC data is done within the ISR. + */ +void loop() +{ +// unsigned long timeNow = millis(); + static byte perSecondTimer = 0; +// + // The ISR provides a 50 Hz 'tick' which the main code is free to use. + if (newMainsCycle) + { + newMainsCycle = false; + perSecondTimer++; + + if(perSecondTimer >= CYCLES_PER_SECOND) + { + perSecondTimer = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // Clear the accumulators for diverted energy. These are the "genuine" + // accumulators that are used by ISR rather than the copies that are + // regularly made available for use by the main code. + // + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); // this timing is not critical so does not need to be in the ISR + } + } + + if (datalogEventPending) + { + datalogEventPending= false; + tx_data.powerAtSupplyPoint_Watts = copyOf_sumP_atSupplyPoint * powerCal_grid / copyOf_sampleSetsDuringThisDatalogPeriod; + tx_data.powerAtSupplyPoint_Watts *= -1; // to match the OEM convention (import is =ve; export is -ve) + tx_data.divertedEnergyTotal_Wh = copyOf_divertedEnergyTotal_Wh; + tx_data.Vrms_times100 = (int)(100 * voltageCal * sqrt(copyOf_sum_Vsquared / copyOf_sampleSetsDuringThisDatalogPeriod)); +// tx_data.temperature_times100 = readTemperature(); + +#ifdef RF_PRESENT + send_rf_data(); +#endif + + Serial.print("datalog event: grid power "); Serial.print(tx_data.powerAtSupplyPoint_Watts); + Serial.print(", diverted energy (Wh) "); Serial.print(tx_data.divertedEnergyTotal_Wh); + Serial.print(", Vrms "); Serial.print((float)tx_data.Vrms_times100 / 100); +// Serial.print(", temperature "); Serial.print((float)tx_data.temperature_times100 / 100); + Serial.print(", (minSampleSets/MC "); Serial.print(copyOf_lowestNoOfSampleSetsPerMainsCycle); + Serial.print(", #ofSampleSets "); Serial.print(copyOf_sampleSetsDuringThisDatalogPeriod); + Serial.println(')'); +// delay(POST_DATALOG_EVENT_DELAY_MILLIS); +// convertTemperature(); // for use next time around + } + +/* + // occasional delays should not affect the operation of this revised code structure. + if (timeNow - timeAtLastDelay > 1000) + { + delay(100); + Serial.println("100ms delay"); + timeAtLastDelay = timeNow; + } +*/ +} + + +ISR(ADC_vect) +/* + * This Interrupt Service Routine looks after the acquisition and processing of + * raw samples from the ADC sub-processor. By means of various helper functions, all of + * the time-critical activities are processed within the ISR. The main code is notified + * by means of a flag when fresh copies of loggable data are available. + */ +{ + static unsigned char sample_index = 0; + int rawSample; + long sampleIminusDC; + long phaseShiftedSampleVminusDC; + long filtV_div4; + long filtI_div4; + long instP; + long inst_Vsquared; + + switch(sample_index) + { + case 0: + rawSample = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // the conversion for I_grid is already under way + sample_index++; // increment the control flag + // + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + sampleVminusDC_long = ((long)rawSample<<8) - DCoffset_V_long; + if(sampleVminusDC_long > 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + // + checkProgress(); // deals with aspects that only occur at particular stages of each mains cycle + // + // for the Vrms calculation (for datalogging only) + filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (now x4096, or 2^12) + inst_Vsquared = inst_Vsquared>>12; // scaling is now x1 (V_ADC x I_ADC) + sum_Vsquared += inst_Vsquared; // cumulative V^2 (V_ADC x I_ADC) + sampleSetsDuringThisDatalogPeriod++; + // + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter +// lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + sampleSetsDuringThisCycle++; // for real power calculations + refreshDisplay(); + break; + case 1: + rawSample = ADC; // store the ADC value (this one is for Grid Current) + ADMUX = 0x40 + voltageSensor; // the conversion for I_diverted is already under way + sample_index++; // increment the control flag + // + // remove most of the DC offset from the current sample (the precise value does not matter) + sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8; + // + // phase-shift the voltage waveform so that it aligns with the grid current waveform + phaseShiftedSampleVminusDC = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + // + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + + sumP_forEnergyBucket+=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + sumP_atSupplyPoint +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + break; + case 2: + rawSample = ADC; // store the ADC value (this one is for Diverted Current) + ADMUX = 0x40 + currentSensor_grid; // the conversion for Voltage is already under way + sample_index = 0; // reset the control flag + // + // remove most of the DC offset from the current sample (the precise value does not matter) + sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8; + // + // phase-shift the voltage waveform so that it aligns with the diverted current waveform + phaseShiftedSampleVminusDC = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + // + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + break; + default: + sample_index = 0; // to prevent lockup (should never get here) + } +} + +/* ----------------------------------------------------------- + * Start of various helper functions which are used by the ISR + */ + +void checkProgress() +/* + * This routine is called by the ISR when each voltage sample becomes available. + * At the start of each new mains cycle, another helper function is called. + * All other processing is done within this function. + */ +{ +// static enum loadStates nextStateOfLoad = LOAD_OFF; + + if (polarityConfirmed == POSITIVE) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + if (beyondStartUpPhase) + { + // The start of a new mains cycle, just after the +ve going zero-crossing point. + + // a simple routine for checking the performance of this new ISR structure + if (sampleSetsDuringThisCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisCycle; } + + processLatestContribution(); // for activities at the start of each new mains cycle + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000) + { + beyondStartUpPhase = true; + sumP_forEnergyBucket = 0; + sumP_atSupplyPoint; + sumP_diverted = 0; + sampleSetsDuringThisCycle = 0; // not yet dealt with for this cycle + sampleSetsDuringThisDatalogPeriod = 0; + // can't say "Go!" here 'cos we're in an ISR! + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + // check to see whether the trigger device can now be reliably armed + // + if (sampleSetsDuringThisCycle == 5) // part way through the +ve half cycle + { + + if (beyondStartUpPhase) + { + /* Determining whether any of the loads need to be changed is is a 3-stage process: + * - change the LOGICAL load states as necessary to maintain the energy level + * - update the PHYSICAL load states according to the logical -> physical mapping + * - update the driver lines for each of the loads. + */ + + // Restrictions apply for the period immediately after a load has been switched. + // Here the recentTransition flag is checked and updated as necessary. + if (recentTransition) + { + postTransitionCount++; + if (postTransitionCount >= POST_TRANSITION_MAX_COUNT) + { + recentTransition = false; + } + } + + if (energyInBucket_long > midPointOfEnergyBucket_long) + { + // the energy state is in the upper half of the working range + lowerEnergyThreshold = lowerThreshold_default; // reset the "opposite" threshold + if (energyInBucket_long > upperEnergyThreshold) + { + // Because the energy level is high, some action may be required + boolean OK_toAddLoad = true; + byte tempLoad = nextLogicalLoadToBeAdded(); + if (tempLoad < noOfDumploads) + { + // a load which is now OFF has been identified for potentially being switched ON + if (recentTransition) + { + // During the post-transition period, any increase in the energy level is noted. + if (energyInBucket_long > upperEnergyThreshold) + { + upperEnergyThreshold = energyInBucket_long; + + // the energy thresholds must remain within range + if (upperEnergyThreshold > capacityOfEnergyBucket_long) + { + upperEnergyThreshold = capacityOfEnergyBucket_long; + } + } + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toAddLoad = false; + } + } + + if (OK_toAddLoad) + { + logicalLoadState[tempLoad] = LOAD_ON; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + } + } + } + } + else + { // the energy state is in the lower half of the working range + upperEnergyThreshold = upperThreshold_default; // reset the "opposite" threshold + if (energyInBucket_long < lowerEnergyThreshold) + { + // Because the energy level is low, some action may be required + boolean OK_toRemoveLoad = true; + byte tempLoad = nextLogicalLoadToBeRemoved(); + if (tempLoad < noOfDumploads) + { + // a load which is now ON has been identified for potentially being switched OFF + if (recentTransition) + { + // During the post-transition period, any decrease in the energy level is noted. + if (energyInBucket_long < lowerEnergyThreshold) + { + lowerEnergyThreshold = energyInBucket_long; + + // the energy thresholds must remain within range + if (lowerEnergyThreshold < 0) + { + lowerEnergyThreshold = 0; + } + } + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toRemoveLoad = false; + } + } + + if (OK_toRemoveLoad) + { + logicalLoadState[tempLoad] = LOAD_OFF; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + } + } + } + } + + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + // update each of the physical loads + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // active high for additional load + + // update the Energy Diversion Detector + if (physicalLoadState[0] == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + + // Now that the energy-related decisions have been taken, min and max limits can now + // be applied to the level of the energy bucket. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + } + } + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12); + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 230V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need for a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + +// checkOutputModeSelection(); // updates outputMode if the external switch is in use + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is negative +} // end of checkProgress() + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count > PERSISTENCE_FOR_POLARITY_CHANGE) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + + +void processLatestContribution() +/* + * This routine runs once per mains cycle. It forms part of the ISR. + */ +{ + newMainsCycle = true; // <-- a 50 Hz 'tick' for use by the main code + + // For the mechanism which controls the diversion of surplus power, the AVERAGE power + // at the 'grid' point during the previous mains cycle must be quantified. The first + // stage in this process is for the sum of all instantaneous power values to be divided + // by the number of sample sets that have contributed to its value. A similar operation + // is required for the diverted power data. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_for_energyBucket = sumP_forEnergyBucket / sampleSetsDuringThisCycle; + long realPower_diverted = sumP_diverted / sampleSetsDuringThisCycle; + // + // The per-mainsCycle variables can now be reset for ongoing use + sampleSetsDuringThisCycle = 0; + sumP_forEnergyBucket = 0; + sumP_diverted = 0; + + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Average power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variables below are CYCLES_PER_SECOND * (1/powerCal) times larger than + // their actual values in Joules. + // + long realEnergy_for_energyBucket = realPower_for_energyBucket; + long realEnergy_diverted = realPower_diverted; + + // The latest energy contribution from the grid connection point can now be added + // to the energy bucket which determines the state of the dump-load. + // + energyInBucket_long += realEnergy_for_energyBucket; + energyInBucket_long -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation + + // Apply max and min limits to the bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. To avoid the displayed + // value from creeping, any small contributions which are likely to be + // caused by noise are ignored. + // + if (realEnergy_diverted > antiCreepLimit_inIEUperMainsCycle) { + divertedEnergyRecent_IEU += realEnergy_diverted; } + + // Whole Watt-Hours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + /* At the end of each datalogging period, copies are made of the relevant variables + * for use by the main code. These variable are then reset for use during the next + * datalogging period. + */ + cycleCountForDatalogging ++; + if (cycleCountForDatalogging >= DATALOG_PERIOD_IN_MAINS_CYCLES ) + { + cycleCountForDatalogging = 0; + + copyOf_sumP_atSupplyPoint = sumP_atSupplyPoint; + copyOf_divertedEnergyTotal_Wh = divertedEnergyTotal_Wh; + copyOf_sum_Vsquared = sum_Vsquared; + copyOf_sampleSetsDuringThisDatalogPeriod = sampleSetsDuringThisDatalogPeriod; // (for diags only) + copyOf_lowestNoOfSampleSetsPerMainsCycle = lowestNoOfSampleSetsPerMainsCycle; // (for diags only) + + sumP_atSupplyPoint = 0; + sum_Vsquared = 0; + lowestNoOfSampleSetsPerMainsCycle = 999; + sampleSetsDuringThisDatalogPeriod = 0; + datalogEventPending = true; + } +} + +byte nextLogicalLoadToBeAdded() +{ + byte retVal = noOfDumploads; + boolean success = false; + for (byte index = 0; index < noOfDumploads && !success; index++) + { + if (logicalLoadState[index] == LOAD_OFF) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +byte nextLogicalLoadToBeRemoved() +{ + byte retVal = noOfDumploads; + boolean success = false; + + // this index counter can't be a 'byte' because the loop would run forever! + // + for (int index = (noOfDumploads -1); index >= 0 && !success; index--) + { + if (logicalLoadState[index] == LOAD_ON) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * The association between the physical and logical loads is 1:1. By default, numerical + * equivalence is maintained, so logical(N) maps to physical(N). If physical load 1 is set + * to have priority, rather than physical load 0, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * Any other mapping relaionships could be configured here. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } + + if (loadPriorityMode == LOAD_1_HAS_PRIORITY) + { + // swap physical loads 0 & 1 if remote load has priority + physicalLoadState[0] = logicalLoadState[1]; + physicalLoadState[1] = logicalLoadState[0]; + } +} + + + + +/* End of helper functions which are used by the ISR + * ------------------------------------------------- + */ + +/* +this function changes the value of outputMode if the external switch is in use for this purpose +void checkOutputModeSelection() +{ + static byte count = 0; + int pinState; + pinState = digitalRead(outputModeSelectorPin); <- pin re-allocated for Dallas sensor + if (pinState != outputMode) + { + count++; + } + if (count >= 20) + { + count = 0; + outputMode = (enum outputModes)pinState; // change the global variable + Serial.print ("outputMode selection changed to "); + if (outputMode == NORMAL) { + Serial.println ( "normal"); } + else { + Serial.println ( "anti-flicker"); } + + configureParamsForSelectedOutputMode(); + } +} +*/ + +/* +// this function changes the value of the load priorities if the state of the external switch is altered +void checkLoadPrioritySelection() +{ + static byte loadPrioritySwitchCount = 0; + int pinState = digitalRead(loadPrioritySelectorPin); + if (pinState != loadPriorityMode) + { + loadPrioritySwitchCount++; + } + if (loadPrioritySwitchCount >= 20) + { + loadPrioritySwitchCount = 0; + loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable + Serial.print ("loadPriority selection changed to "); + if (loadPriorityMode == LOAD_0_HAS_PRIORITY) { + Serial.println ( "load 0"); } + else { + Serial.println ( "load 1"); } + } +} +*/ +void configureParamsForSelectedOutputMode() +/* + * retained for compatibility with previous versions + */ +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerThreshold_default = + capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperThreshold_default = + capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerThreshold_default = capacityOfEnergyBucket_long * 0.5; + upperThreshold_default = capacityOfEnergyBucket_long * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" lowerEnergyThreshold = "); + Serial.println(lowerThreshold_default); + Serial.print(" upperEnergyThreshold = "); + Serial.println(upperThreshold_default); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +} + + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + +/* + * void convertTemperature() +{ + oneWire.reset(); + oneWire.write(SKIP_ROM); + oneWire.write(CONVERT_TEMPERATURE); +} + +int readTemperature() +{ + byte buf[9]; + int result; + + oneWire.reset(); + oneWire.write(SKIP_ROM); + oneWire.write(READ_SCRATCHPAD); + for(int i=0; i<9; i++) buf[i]=oneWire.read(); + if(oneWire.crc8(buf,8)==buf[8]) + { + result=(buf[1]<<8)|buf[0]; + // result is temperature x16, multiply by 6.25 to convert to temperature x100 + result=(result*6)+(result>>2); + } + else result=BAD_TEMPERATURE; + return result; +} +*/ + +#ifdef RF_PRESENT +void send_rf_data() +// +// To avoid disturbance to the sampling process, the RFM12B needs to remain in its +// active state rather than being periodically put to sleep. +{ + // check whether it's ready to send, and an exit route if it gets stuck + int i = 0; + while (!rf12_canSend() && i<10) + { + rf12_recvDone(); + i++; + } + rf12_sendNow(0, &tx_data, sizeof tx_data); +} +#endif + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_multiLoad_1a.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_multiLoad_1a.ino new file mode 100644 index 0000000..70c437d --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_RFdatalog_multiLoad_1a.ino @@ -0,0 +1,1400 @@ +/* Mk2_RFdatalog_multiLoad_1a.ino + * + * This sketch is for diverting suplus PV power to one or two dump loads using + * triac-based output stages or Solid State Relays. Routine datalogging is + * avalable if a suitable RF module is fitted (either RFM12B or RF69). A 4-digit display + * showing the Diverted Energy each day is also supported. + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The integral voltage sensor is fed from one of the secondary coils of the + * transformer. Current is measured via Current Transformers at the + * CT1 and CT1 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. When the RF module is + * in use, the display can only be used in conjunction with an extra pair of + * logic chips. These are ICs 3 and 4, which reduce the number of processor pins + * that are needed to drive the display. + * + * This sketch is based on the Mk2i PV Router code that I have posted in on the + * OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * September 2014: renamed as Mk2_RFdatalog_3, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - tidier initialisation of display logic in setup(); + * + * December 2014, upgraded to Mk2_RFdatalog_4: + * This sketch has been restructured in order to make better use of the ISR. All of + * the time-critical code is now contained within the ISR and its helper functions. + * Values for datalogging are transferred to the main code using a flag-based handshake + * mechanism. The diversion of surplus power can no longer be affected by slower + * activities which may be running in the main code such as Serial statements and RF. + * Temperature sensing is supported by re-allocating the "mode" port for this + * purpose. A pullup resistor (4K7 or similar) is required for the Dallas sensor. The + * output mode, i.e. NORMAL or ANTI_FLICKER, is now set at compile time. + * Also: + * - The ADC is now in free-running mode, at ~104 us per conversion. + * - a persistence check has been added for zero-crossing detection (polarityConfirmed) + * - a lowestNoOfSampleSetsPerMainsCycle check has been added, to detect any disturbances + * - Vrms has been added to the datalog payload (as Vrms x 100) + * - temperature has been added to the datalog payload (as degrees C x 100) + * - the phaseCal mechanism has been reinstated + * + * January 2016: renamed as Mk2_RFdatalog_4a, with a minor change in the ISR to reinstate + * the phaseCal calculation. Previously, this feature was having no effect because + * two assignment lines were in the wrong order. When measuring "real power", which + * is what this application does, the phaseCal refinement has very little effect even + * when correctly implemented, as it now is. + * Support for the RF69 RF module has also been added. + * + * January 2016: updated to Mk2_RFdatalog_4b: + * The variables to store copies of ADC results for use by the main code are now declared + * as "volatile" to remove any possibility of incorrect operation due to optimisation + * by the compiler. + * + * February 2016: updated to Mk2_RFdatalog_5, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * March 2016: updated to Mk2_RFdatalog_5a, with this change: + * - RF capability made switchable so that the code will continue to run + * when an RF module is not fitted. Dataloging can then take place + * via the Serial port. + * + * October 2017: updated to Mk2_RFdatalog_multiLoad_1, with these changes: + * - temperature sensing commented out(formally supported via D3 at the "mode" port") + * - support for a second load added (vcontrolled via D3 at the "mode" port") + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include +// #include // for temperature sensing + +#define RF_PRESENT // <- this line must be commented out if the RFM12B module is not present + +#ifdef RF_PRESENT +#define RF69_COMPAT 0 // for the RFM12B +// #define RF69_COMPAT 1 // for the RF69 +#include +#endif + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// ----------------------------------------------------- +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define WORKING_RANGE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +/* +// -------------------------- +// Dallas DS18B20 commands +#define SKIP_ROM 0xcc +#define CONVERT_TEMPERATURE 0x44 +#define READ_SCRATCHPAD 0xbe +#define BAD_TEMPERATURE 30000 // this value (300C) is sent if no sensor is present +*/ + +// ---------------- +// general literals +#define DATALOG_PERIOD_IN_MAINS_CYCLES 250 +#define ANTI_CREEP_LIMIT 0 // <- to prevent the diverted energy total from 'creeping' + // in Joules per mains cycle (has no effect when set to 0) + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +const byte noOfDumploads = 2; + +// ------------------------------- +// definitions of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum outputModes {ANTI_FLICKER, NORMAL}; // retained for compatibility with previous versions. +enum loadPriorityModes {LOAD_1_HAS_PRIORITY, LOAD_0_HAS_PRIORITY}; + +enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are active low +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +// ---- Output mode selection ----- +// enum outputModes outputMode = ANTI_FLICKER; // <- needs to be set here unless an +enum outputModes outputMode = NORMAL; // external switch is in use + +enum loadPriorityModes loadPriorityMode = LOAD_0_HAS_PRIORITY; // <- needs to be set here unless an +// enum loadPriorityModes loadPriorityMode = LOAD_1_HAS_PRIORITY; // <- external switch is in use + +/* -------------------------------------- + * RF configuration (for the RFM12B module) + * frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ + */ +#ifdef RF_PRESENT +#define freq RF12_868MHZ + +const int nodeID = 10; // RFM12B node ID +const int networkGroup = 210; // wireless network group - needs to be same for all nodes +const int UNO = 1; // for when the processor contains the UNO bootloader. +#endif + +typedef struct { + int powerAtSupplyPoint_Watts; // import = +ve, to match OEM convention + int divertedEnergyTotal_Wh; // always positive + int Vrms_times100; +// int temperature_times100; +} Tx_struct; +Tx_struct tx_data; + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +// D2 is for the RFM12B +//const byte tempSensorPin = 3; // <-- the "mode" port +const byte physicalLoad_1_pin = 3; // <-- the "mode" port is active high +const byte physicalLoad_0_pin = 4; // <-- the "trigger" port is active low +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +// D10 is for the RFM12B +// D11 is for the RFM12B +// D12 is for the RFM12B +// D13 is for the RFM12B + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + +const byte delayBeforeSerialStarts = 2; // in seconds, to allow Serial window to be opened +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +/* ------------------------------------------------------------------------------------- + * Global variables that are used in multiple blocks so cannot be static. + * For integer maths, many variables need to be 'long' + */ +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long energyInBucket_long = 0; // in Integer Energy Units (for controlling the dump-load) +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long midPointOfEnergyBucket_long; // used for 'normal' and single-threshold 'AF' logic +int phaseCal_grid_int; // to avoid the need for floating-point maths +int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF + +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; +float IEUtoJoulesConversion_CT1; + +long lowerThreshold_default; +long lowerEnergyThreshold; +long upperThreshold_default; +long upperEnergyThreshold; + +boolean recentTransition; +byte postTransitionCount; +#define POST_TRANSITION_MAX_COUNT 3 // <-- allows each transition to take effect +byte activeLoad = 0; + +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- not wise to exceeed 0.4 + +long sumP_forEnergyBucket; // for per-cycle summation of 'real power' +long sumP_diverted; // for per-cycle summation of diverted power +long sumP_atSupplyPoint; // for summation of 'real power' values during datalog period +long sum_Vsquared; // for summation of V^2 values during datalog period +int sampleSetsDuringThisCycle; // for counting the sample sets during each mains cycle +long sampleSetsDuringThisDatalogPeriod; // for counting the sample sets during each datalogging period + +long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) +long lastSampleVminusDC_long; // for the phaseCal algorithm +byte cycleCountForDatalogging = 0; +long sampleVminusDC_long; +long requiredExportPerMainsCycle_inIEU; + +// for interaction between the main code and the ISR +volatile boolean datalogEventPending = false; +volatile boolean newMainsCycle = false; +volatile long copyOf_sumP_atSupplyPoint; +volatile long copyOf_sum_Vsquared; +volatile long copyOf_divertedEnergyTotal_Wh; +volatile int copyOf_lowestNoOfSampleSetsPerMainsCycle; +volatile long copyOf_sampleSetsDuringThisDatalogPeriod; + +/* + * // For temperature sensing +OneWire oneWire(tempSensorPin); +int tempTimes100; +*/ + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define PERSISTENCE_FOR_POLARITY_CHANGE 2 +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; // for zero-crossing detection +enum polarities polarityConfirmedOfLastSampleV; // for zero-crossing detection + +// For a mechanism to check the integrity of this code structure +int lowestNoOfSampleSetsPerMainsCycle; +unsigned long timeAtLastDelay; + + +// Calibration values (not important for the Router's basic operation) +//------------------- +// For accurate calculation of real power/energy, two calibration values are +// used: powerCal and phaseCal. With most hardware, the default values are +// likely to work fine without need for change. A full explanation of each +// of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool_2chan.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. Any pre-built system that I supply will have been +// checked with this tool to ensure that the input sensors are working correctly. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 230V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 230V AC has a peak-to-peak amplitude of 651V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +// for 3.3V operation, the optimum value is generally around 0.044 +// for 5V operation, the optimum value is generally around 0.072 +// +const float powerCal_grid = 0.072; +const float powerCal_diverted = 0.073; + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. This mechanism can be used to offset any difference in +// phase delay between the voltage and current sensors. The algorithm interpolates +// between the most recent pair of voltage samples according to the phaseCal value. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value is used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. +// +// The calculation for real power is very insensitive to the value of phaseCal. +// When a "real power" calculation is used to determine how much surplus energy +// is available for diversion, a nominal value such as 1.0 is generally thought +// to be sufficient for this purpose. +// +const float phaseCal_grid = 1.0; +const float phaseCal_diverted = 1.0; + + +// For datalogging purposes, voltageCal has been included too. When running at +// 230 V AC, the range of ADC values will be similar to the actual range of volts, +// so the optimal value for this cal factor will be close to unity. +// +const float voltageCal = 1.0; + + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of this array is for the decimal point status. +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +volatile boolean EDD_isActive = false; // energy diversion detection +//volatile boolean EDD_isActive = true; // (for test purposes only) + + +void setup() +{ + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for the local dump-load + pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for an additional load + + for(int i = 0; i< noOfDumploads; i++) + { + logicalLoadState[i] = LOAD_OFF; + physicalLoadState[i] = LOAD_OFF; + } + + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // the local load is active low. + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // additional loads are active high. + +/* + pinMode(loadPrioritySelectorPin, INPUT); // this pin is tracked to the "mode" connector + digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and + loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority +*/ + + delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_RFdatalog_5a.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, calibration values that are supplied in floating point + // form need to be rescaled. + // + phaseCal_grid_int = phaseCal_grid * 256; // for integer maths + phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail just before the energy bucket is updated at the start + // of each new mains cycle. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone's value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1): + capacityOfEnergyBucket_long = + (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + midPointOfEnergyBucket_long = capacityOfEnergyBucket_long / 2; + IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; // may be useful + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled in a known way. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_diverted); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.println ("ADC mode: free-running"); + + // Set up the ADC to be free-running + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + configureParamsForSelectedOutputMode(); + Serial.println ("----"); + +// convertTemperature(); // start initial temperature conversion + + Serial.print ("RF capability "); + +#ifdef RF_PRESENT + Serial.print ("IS present, freq = "); + if (freq == RF12_433MHZ) { Serial.println ("433 MHz"); } + if (freq == RF12_868MHZ) { Serial.println ("868 MHz"); } + rf12_initialize(nodeID, freq, networkGroup); // initialize RF +#else + Serial.println ("is NOT present"); +#endif + +// convertTemperature(); // start initial temperature conversion +} + +/* None of the workload in loop() is time-critical. All the processing of + * ADC data is done within the ISR. + */ +void loop() +{ +// unsigned long timeNow = millis(); + static byte perSecondTimer = 0; +// + // The ISR provides a 50 Hz 'tick' which the main code is free to use. + if (newMainsCycle) + { + newMainsCycle = false; + perSecondTimer++; + + if(perSecondTimer >= CYCLES_PER_SECOND) + { + perSecondTimer = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // Clear the accumulators for diverted energy. These are the "genuine" + // accumulators that are used by ISR rather than the copies that are + // regularly made available for use by the main code. + // + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); // this timing is not critical so does not need to be in the ISR + } + } + + if (datalogEventPending) + { + datalogEventPending= false; + tx_data.powerAtSupplyPoint_Watts = copyOf_sumP_atSupplyPoint * powerCal_grid / copyOf_sampleSetsDuringThisDatalogPeriod; + tx_data.powerAtSupplyPoint_Watts *= -1; // to match the OEM convention (import is =ve; export is -ve) + tx_data.divertedEnergyTotal_Wh = copyOf_divertedEnergyTotal_Wh; + tx_data.Vrms_times100 = (int)(100 * voltageCal * sqrt(copyOf_sum_Vsquared / copyOf_sampleSetsDuringThisDatalogPeriod)); +// tx_data.temperature_times100 = readTemperature(); + +#ifdef RF_PRESENT + send_rf_data(); +#endif + + Serial.print("datalog event: grid power "); Serial.print(tx_data.powerAtSupplyPoint_Watts); + Serial.print(", diverted energy (Wh) "); Serial.print(tx_data.divertedEnergyTotal_Wh); + Serial.print(", Vrms "); Serial.print((float)tx_data.Vrms_times100 / 100); +// Serial.print(", temperature "); Serial.print((float)tx_data.temperature_times100 / 100); + Serial.print(", (minSampleSets/MC "); Serial.print(copyOf_lowestNoOfSampleSetsPerMainsCycle); + Serial.print(", #ofSampleSets "); Serial.print(copyOf_sampleSetsDuringThisDatalogPeriod); + Serial.println(')'); +// delay(POST_DATALOG_EVENT_DELAY_MILLIS); +// convertTemperature(); // for use next time around + } + +/* + // occasional delays should not affect the operation of this revised code structure. + if (timeNow - timeAtLastDelay > 1000) + { + delay(100); + Serial.println("100ms delay"); + timeAtLastDelay = timeNow; + } +*/ +} + + +ISR(ADC_vect) +/* + * This Interrupt Service Routine looks after the acquisition and processing of + * raw samples from the ADC sub-processor. By means of various helper functions, all of + * the time-critical activities are processed within the ISR. The main code is notified + * by means of a flag when fresh copies of loggable data are available. + */ +{ + static unsigned char sample_index = 0; + int rawSample; + long sampleIminusDC; + long phaseShiftedSampleVminusDC; + long filtV_div4; + long filtI_div4; + long instP; + long inst_Vsquared; + + switch(sample_index) + { + case 0: + rawSample = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // the conversion for I_grid is already under way + sample_index++; // increment the control flag + // + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + sampleVminusDC_long = ((long)rawSample<<8) - DCoffset_V_long; + if(sampleVminusDC_long > 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + // + checkProgress(); // deals with aspects that only occur at particular stages of each mains cycle + // + // for the Vrms calculation (for datalogging only) + filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (now x4096, or 2^12) + inst_Vsquared = inst_Vsquared>>12; // scaling is now x1 (V_ADC x I_ADC) + sum_Vsquared += inst_Vsquared; // cumulative V^2 (V_ADC x I_ADC) + sampleSetsDuringThisDatalogPeriod++; + // + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter +// lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + sampleSetsDuringThisCycle++; // for real power calculations + refreshDisplay(); + break; + case 1: + rawSample = ADC; // store the ADC value (this one is for Grid Current) + ADMUX = 0x40 + voltageSensor; // the conversion for I_diverted is already under way + sample_index++; // increment the control flag + // + // remove most of the DC offset from the current sample (the precise value does not matter) + sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8; + // + // phase-shift the voltage waveform so that it aligns with the grid current waveform + phaseShiftedSampleVminusDC = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + // + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + + sumP_forEnergyBucket+=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + sumP_atSupplyPoint +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + break; + case 2: + rawSample = ADC; // store the ADC value (this one is for Diverted Current) + ADMUX = 0x40 + currentSensor_grid; // the conversion for Voltage is already under way + sample_index = 0; // reset the control flag + // + // remove most of the DC offset from the current sample (the precise value does not matter) + sampleIminusDC = ((long)(rawSample-DCoffset_I))<<8; + // + // phase-shift the voltage waveform so that it aligns with the diverted current waveform + phaseShiftedSampleVminusDC = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + // + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + break; + default: + sample_index = 0; // to prevent lockup (should never get here) + } +} + +/* ----------------------------------------------------------- + * Start of various helper functions which are used by the ISR + */ + +void checkProgress() +/* + * This routine is called by the ISR when each voltage sample becomes available. + * At the start of each new mains cycle, another helper function is called. + * All other processing is done within this function. + */ +{ +// static enum loadStates nextStateOfLoad = LOAD_OFF; + + if (polarityConfirmed == POSITIVE) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + if (beyondStartUpPhase) + { + // The start of a new mains cycle, just after the +ve going zero-crossing point. + + // a simple routine for checking the performance of this new ISR structure + if (sampleSetsDuringThisCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisCycle; } + + processLatestContribution(); // for activities at the start of each new mains cycle + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000) + { + beyondStartUpPhase = true; + sumP_forEnergyBucket = 0; + sumP_atSupplyPoint; + sumP_diverted = 0; + sampleSetsDuringThisCycle = 0; // not yet dealt with for this cycle + sampleSetsDuringThisDatalogPeriod = 0; + // can't say "Go!" here 'cos we're in an ISR! + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + // check to see whether the trigger device can now be reliably armed + // + if (sampleSetsDuringThisCycle == 5) // part way through the +ve half cycle + { + + if (beyondStartUpPhase) + { + /* Determining whether any of the loads need to be changed is is a 3-stage process: + * - change the LOGICAL load states as necessary to maintain the energy level + * - update the PHYSICAL load states according to the logical -> physical mapping + * - update the driver lines for each of the loads. + */ + + // Restrictions apply for the period immediately after a load has been switched. + // Here the recentTransition flag is checked and updated as necessary. + if (recentTransition) + { + postTransitionCount++; + if (postTransitionCount >= POST_TRANSITION_MAX_COUNT) + { + recentTransition = false; + } + } + + if (energyInBucket_long > midPointOfEnergyBucket_long) + { + // the energy state is in the upper half of the working range + lowerEnergyThreshold = lowerThreshold_default; // reset the "opposite" threshold + if (energyInBucket_long > upperEnergyThreshold) + { + // Because the energy level is high, some action may be required + boolean OK_toAddLoad = true; + byte tempLoad = nextLogicalLoadToBeAdded(); + if (tempLoad < noOfDumploads) + { + // a load which is now OFF has been identified for potentially being switched ON + if (recentTransition) + { + // During the post-transition period, any increase in the energy level is noted. + if (energyInBucket_long > upperEnergyThreshold) + { + upperEnergyThreshold = energyInBucket_long; + + // the energy thresholds must remain within range + if (upperEnergyThreshold > capacityOfEnergyBucket_long) + { + upperEnergyThreshold = capacityOfEnergyBucket_long; + } + } + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toAddLoad = false; + } + } + + if (OK_toAddLoad) + { + logicalLoadState[tempLoad] = LOAD_ON; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + } + } + } + } + else + { // the energy state is in the lower half of the working range + upperEnergyThreshold = upperThreshold_default; // reset the "opposite" threshold + if (energyInBucket_long < lowerEnergyThreshold) + { + // Because the energy level is low, some action may be required + boolean OK_toRemoveLoad = true; + byte tempLoad = nextLogicalLoadToBeRemoved(); + if (tempLoad < noOfDumploads) + { + // a load which is now ON has been identified for potentially being switched OFF + if (recentTransition) + { + // During the post-transition period, any decrease in the energy level is noted. + if (energyInBucket_long < lowerEnergyThreshold) + { + lowerEnergyThreshold = energyInBucket_long; + + // the energy thresholds must remain within range + if (lowerEnergyThreshold < 0) + { + lowerEnergyThreshold = 0; + } + } + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toRemoveLoad = false; + } + } + + if (OK_toRemoveLoad) + { + logicalLoadState[tempLoad] = LOAD_OFF; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + } + } + } + } + + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + // update each of the physical loads + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // active high for additional load + + // update the Energy Diversion Detector + if (physicalLoadState[0] == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + + // Now that the energy-related decisions have been taken, min and max limits can now + // be applied to the level of the energy bucket. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + } + } + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12); + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 230V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need for a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + +// checkOutputModeSelection(); // updates outputMode if the external switch is in use + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is negative +} // end of checkProgress() + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count > PERSISTENCE_FOR_POLARITY_CHANGE) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + + +void processLatestContribution() +/* + * This routine runs once per mains cycle. It forms part of the ISR. + */ +{ + newMainsCycle = true; // <-- a 50 Hz 'tick' for use by the main code + + // For the mechanism which controls the diversion of surplus power, the AVERAGE power + // at the 'grid' point during the previous mains cycle must be quantified. The first + // stage in this process is for the sum of all instantaneous power values to be divided + // by the number of sample sets that have contributed to its value. A similar operation + // is required for the diverted power data. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_for_energyBucket = sumP_forEnergyBucket / sampleSetsDuringThisCycle; + long realPower_diverted = sumP_diverted / sampleSetsDuringThisCycle; + // + // The per-mainsCycle variables can now be reset for ongoing use + sampleSetsDuringThisCycle = 0; + sumP_forEnergyBucket = 0; + sumP_diverted = 0; + + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Average power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variables below are CYCLES_PER_SECOND * (1/powerCal) times larger than + // their actual values in Joules. + // + long realEnergy_for_energyBucket = realPower_for_energyBucket; + long realEnergy_diverted = realPower_diverted; + + // The latest energy contribution from the grid connection point can now be added + // to the energy bucket which determines the state of the dump-load. + // + energyInBucket_long += realEnergy_for_energyBucket; + energyInBucket_long -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation + + // Apply max and min limits to the bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. To avoid the displayed + // value from creeping, any small contributions which are likely to be + // caused by noise are ignored. + // + if (realEnergy_diverted > antiCreepLimit_inIEUperMainsCycle) { + divertedEnergyRecent_IEU += realEnergy_diverted; } + + // Whole Watt-Hours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + /* At the end of each datalogging period, copies are made of the relevant variables + * for use by the main code. These variable are then reset for use during the next + * datalogging period. + */ + cycleCountForDatalogging ++; + if (cycleCountForDatalogging >= DATALOG_PERIOD_IN_MAINS_CYCLES ) + { + cycleCountForDatalogging = 0; + + copyOf_sumP_atSupplyPoint = sumP_atSupplyPoint; + copyOf_divertedEnergyTotal_Wh = divertedEnergyTotal_Wh; + copyOf_sum_Vsquared = sum_Vsquared; + copyOf_sampleSetsDuringThisDatalogPeriod = sampleSetsDuringThisDatalogPeriod; // (for diags only) + copyOf_lowestNoOfSampleSetsPerMainsCycle = lowestNoOfSampleSetsPerMainsCycle; // (for diags only) + + sumP_atSupplyPoint = 0; + sum_Vsquared = 0; + lowestNoOfSampleSetsPerMainsCycle = 999; + sampleSetsDuringThisDatalogPeriod = 0; + datalogEventPending = true; + } +} + +byte nextLogicalLoadToBeAdded() +{ + byte retVal = noOfDumploads; + boolean success = false; + for (byte index = 0; index < noOfDumploads && !success; index++) + { + if (logicalLoadState[index] == LOAD_OFF) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +byte nextLogicalLoadToBeRemoved() +{ + byte retVal = noOfDumploads; + boolean success = false; + + // this index counter can't be a 'byte' because the loop would run forever! + // + for (int index = (noOfDumploads -1); index >= 0 && !success; index--) + { + if (logicalLoadState[index] == LOAD_ON) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * The association between the physical and logical loads is 1:1. By default, numerical + * equivalence is maintained, so logical(N) maps to physical(N). If physical load 1 is set + * to have priority, rather than physical load 0, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * Any other mapping relaionships could be configured here. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } + + if (loadPriorityMode == LOAD_1_HAS_PRIORITY) + { + // swap physical loads 0 & 1 if remote load has priority + physicalLoadState[0] = logicalLoadState[1]; + physicalLoadState[1] = logicalLoadState[0]; + } +} + + + + +/* End of helper functions which are used by the ISR + * ------------------------------------------------- + */ + +/* +this function changes the value of outputMode if the external switch is in use for this purpose +void checkOutputModeSelection() +{ + static byte count = 0; + int pinState; + pinState = digitalRead(outputModeSelectorPin); <- pin re-allocated for Dallas sensor + if (pinState != outputMode) + { + count++; + } + if (count >= 20) + { + count = 0; + outputMode = (enum outputModes)pinState; // change the global variable + Serial.print ("outputMode selection changed to "); + if (outputMode == NORMAL) { + Serial.println ( "normal"); } + else { + Serial.println ( "anti-flicker"); } + + configureParamsForSelectedOutputMode(); + } +} +*/ + +/* +// this function changes the value of the load priorities if the state of the external switch is altered +void checkLoadPrioritySelection() +{ + static byte loadPrioritySwitchCount = 0; + int pinState = digitalRead(loadPrioritySelectorPin); + if (pinState != loadPriorityMode) + { + loadPrioritySwitchCount++; + } + if (loadPrioritySwitchCount >= 20) + { + loadPrioritySwitchCount = 0; + loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable + Serial.print ("loadPriority selection changed to "); + if (loadPriorityMode == LOAD_0_HAS_PRIORITY) { + Serial.println ( "load 0"); } + else { + Serial.println ( "load 1"); } + } +} +*/ +void configureParamsForSelectedOutputMode() +/* + * retained for compatibility with previous versions + */ +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerThreshold_default = + capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperThreshold_default = + capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerThreshold_default = capacityOfEnergyBucket_long * 0.5; + upperThreshold_default = capacityOfEnergyBucket_long * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" lowerEnergyThreshold = "); + Serial.println(lowerThreshold_default); + Serial.print(" upperEnergyThreshold = "); + Serial.println(upperThreshold_default); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +} + + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + +/* + * void convertTemperature() +{ + oneWire.reset(); + oneWire.write(SKIP_ROM); + oneWire.write(CONVERT_TEMPERATURE); +} + +int readTemperature() +{ + byte buf[9]; + int result; + + oneWire.reset(); + oneWire.write(SKIP_ROM); + oneWire.write(READ_SCRATCHPAD); + for(int i=0; i<9; i++) buf[i]=oneWire.read(); + if(oneWire.crc8(buf,8)==buf[8]) + { + result=(buf[1]<<8)|buf[0]; + // result is temperature x16, multiply by 6.25 to convert to temperature x100 + result=(result*6)+(result>>2); + } + else result=BAD_TEMPERATURE; + return result; +} +*/ + +#ifdef RF_PRESENT +void send_rf_data() +// +// To avoid disturbance to the sampling process, the RFM12B needs to remain in its +// active state rather than being periodically put to sleep. +{ + // check whether it's ready to send, and an exit route if it gets stuck + int i = 0; + while (!rf12_canSend() && i<10) + { + rf12_recvDone(); + i++; + } + rf12_sendNow(0, &tx_data, sizeof tx_data); +} +#endif + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_1.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_1.ino new file mode 100644 index 0000000..e0ce267 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_1.ino @@ -0,0 +1,1093 @@ +/* Mk2_bothDisplays_1.ino + * + * This sketch is for diverting surplus PV power to a dump load using a triac. + * It is based on the Mk2i PV Router code that I have posted in on the + * OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * In this latest version, the pin-allocations have been changed to suit my + * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is + * fed from one of the secondary coils of the transformer. Current is measured + * via Current Transformers at the CT1 and CT2 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. This can be driven in two + * different ways, one with an extra pair of logic chips, and one without. The + * appropriate version of the sketch must be selected by including or commenting + * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + * February 2014 + */ + +#include + +#include +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define SWEETZONE_IN_JOULES 3600 + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +// The two versions of the hardware require different logic. The following line should +// be included if the additional logic chips are present, or excluded if they are +// absent (in which case some wire links need to be fitted) +// +//#define PIN_SAVING_HARDWARE + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum triacStates {TRIAC_ON, TRIAC_OFF}; // the external trigger device is active low +enum outputModes {ANTI_FLICKER, NORMAL}; + +// ---------------- Extra Features selection ---------------------- +// +// - WORKLOAD_CHECK, for determining how much spare processing time there is. +// +// #define WORKLOAD_CHECK // <-- Include this line is this feature is required + + +// The power-diversion logic can operate in either of two modes: +// +// - NORMAL, where the triac switches rapidly on/off to maintain a constant energy level. +// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations +// of the local mains voltage. +// +// The output mode is determined in realtime via a selector switch +enum outputModes outputMode; + +// allocation of digital pins for prototype PCB-based rig (with simple display adapter) +// ****************************************************** +// D0 & D1 are reserved for the Serial i/f +// D2 is a driver line for the 4-digit display (segment D, via series resistor) +const byte outputModeSelectorPin = 3; // <-- with the internal pullup +const byte outputForTrigger = 4; +// D5 is a driver line for the 4-digit display (segment B, via series resistor) +// D6 is a driver line for the 4-digit display (digit 3, via wire link) +// D7 is a driver line for the 4-digit display (digit 2, via wire link) +// D8 is a driver line for the 4-digit display (segment F, via series resistor) +// D9 is a driver line for the 4-digit display (segment A, via series resistor) +// D10 is a driver line for the 4-digit display (segment DP, via series resistor) +// D11 is a driver line for the 4-digit display (segment C, via series resistor) +// D12 is a driver line for the 4-digit display (segment G, via series resistor) +// D13 is a driver line for the 4-digit display (digit 4, via wire link) + +// allocation of analogue pins +// *************************** +// A0 (D14) is a driver line for the 4-digit display (digit 1, via wire link) +// A1 (D15) is a driver line for the 4-digit display (segment E, via series resistor) +// A2 (D16) is unused (it's routed to pin 1 of IC4 which is not fitted) +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + + +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long cycleCount = 0; // counts mains cycles from start-up +long triggerThreshold_long; // for determining when the trigger may be safely armed +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long lowerEnergyThreshold_long; // for turning triac off +long upperEnergyThreshold_long; // for turning triac on +// int phaseCal_grid_int; // to avoid the need for floating-point maths +// int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; +long mainsCyclesPerHour; + +// this setting is only used if anti-flicker mode is enabled +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.5 + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +int sampleI_grid; +int sampleI_diverted; +int sampleV; + + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// With most hardware, the default values are likely to work fine without +// need for change. A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When +// measuring a resistive load, the voltage and current waveforms should be perfectly +// aligned. In this situation, the Power Factor will be 1. +// +// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct +// value of phaseCal for any hardware configuration. An index of my various Mk2-related +// exhibits is available at http://openenergymonitor.org/emon/node/1757 +// +//const float phaseCal_grid = 1.0; <--- not used in this version +//const float phaseCal_diverted = 1.0; <--- not used in this version + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define UPDATE_PERIOD_FOR_DISPLAYED_DATA 50 // mains cycles +#define DISPLAY_SHUTDOWN_IN_HOURS 10 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +// The two versions of the hardware require different logic. +#ifdef PIN_SAVING_HARDWARE + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. In this version, +// the decimal point has to be treated differently than the other seven segments, so +// a convenient means of accessing this column is provided. +// +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + +#else // PIN_SAVING_HARDWARE + +#define ON HIGH +#define OFF LOW + +const byte noOfSegmentsPerDigit = 8; // includes one for the decimal point +enum digitEnableStates {DIGIT_ENABLED, DIGIT_DISABLED}; + +byte digitSelectorPin[noOfDigitLocations] = {16,10,13,11}; +byte segmentDrivePin[noOfSegmentsPerDigit] = {2,5,12,6,7,9,8,14}; + +// The final column of segMap[] is for the decimal point status. In this version, +// the decimal point is treated just like all the other segments, so there is +// no need to access this column specifically. +// +byte segMap[noOfPossibleCharacters][noOfSegmentsPerDigit] = { + ON , ON , ON , ON , ON , ON , OFF, OFF, // '0' <- element 0 + OFF, ON , ON , OFF, OFF, OFF, OFF, OFF, // '1' <- element 1 + ON , ON , OFF, ON , ON , OFF, ON , OFF, // '2' <- element 2 + ON , ON , ON , ON , OFF, OFF, ON , OFF, // '3' <- element 3 + OFF, ON , ON , OFF, OFF, ON , ON , OFF, // '4' <- element 4 + ON , OFF, ON , ON , OFF, ON , ON , OFF, // '5' <- element 5 + ON , OFF, ON , ON , ON , ON , ON , OFF, // '6' <- element 6 + ON , ON , ON , OFF, OFF, OFF, OFF, OFF, // '7' <- element 7 + ON , ON , ON , ON , ON , ON , ON , OFF, // '8' <- element 8 + ON , ON , ON , ON , OFF, ON , ON , OFF, // '9' <- element 9 + ON , ON , ON , ON , ON , ON , OFF, ON , // '0.' <- element 10 + OFF, ON , ON , OFF, OFF, OFF, OFF, ON , // '1.' <- element 11 + ON , ON , OFF, ON , ON , OFF, ON , ON , // '2.' <- element 12 + ON , ON , ON , ON , OFF, OFF, ON , ON , // '3.' <- element 13 + OFF, ON , ON , OFF, OFF, ON , ON , ON , // '4.' <- element 14 + ON , OFF, ON , ON , OFF, ON , ON , ON , // '5.' <- element 15 + ON , OFF, ON , ON , ON , ON , ON , ON , // '6.' <- element 16 + ON , ON , ON , OFF, OFF, OFF, OFF, ON , // '7.' <- element 17 + ON , ON , ON , ON , ON , ON , ON , ON , // '8.' <- element 18 + ON , ON , ON , ON , OFF, ON , ON , ON , // '9.' <- element 19 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, // ' ' <- element 20 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, ON // '.' <- element 11 +}; +#endif // PIN_SAVING_HARDWARE + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // energy divertion detection + + +void setup() +{ + pinMode(outputForTrigger, OUTPUT); + digitalWrite (outputForTrigger, TRIAC_OFF); // the external trigger is active low + + pinMode(outputModeSelectorPin, INPUT); + digitalWrite(outputModeSelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(outputModeSelectorPin); // initial selection and + outputMode = (enum outputModes)pinState; // assignment of output mode + + delay(5000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_bothDisplays_1.ino"); + Serial.println(); + +#ifdef PIN_SAVING_HARDWARE + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // control lines for the 74HC4543 7-seg display driver and the DP line + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + // control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } +#else + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + pinMode(segmentDrivePin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + pinMode(digitSelectorPin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + digitalWrite(digitSelectorPin[i], DIGIT_DISABLED); } + + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + digitalWrite(segmentDrivePin[i], OFF); } +#endif + + + + // When using integer maths, calibration values that have supplied in floating point + // form need to be rescaled. + // +// phaseCal_grid_int = phaseCal_grid * 256; // for integer maths +// phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + configureParamsForSelectedOutputMode(); + + Serial.println ("----"); + +#ifdef WORKLOAD_CHECK + Serial.println ("WELCOME TO WORKLOAD_CHECK "); + +// <<- start of commented out section, to save on RAM space! +/* + Serial.println (" This mode of operation allows the spare processing capacity of the system"); + Serial.println ("to be analysed. Additional delay is gradually increased until all spare time"); + Serial.println ("has been used up. This value (in uS) is noted and the process is repeated. "); + Serial.println ("The delay setting is increased by 1uS at a time, and each value of delay is "); + Serial.println ("checked several times before the delay is increased. "); + */ +// <<- end of commented out section, to save on RAM space! + + Serial.println (" The displayed value is the amount of spare time, per set of V & I samples, "); + Serial.println ("that is available for doing additional processing."); + Serial.println (); + #endif +} + +// An Interrupt Service Routine is now defined in which the ADC is instructed to +// measure V and I alternately. A "data ready"flag is set after each voltage conversion +// has been completed. +// For each pair of samples, this means that current is measured before voltage. The +// current sample is taken first because the phase of the waveform for current is generally +// slightly advanced relative to the waveform for voltage. The data ready flag is cleared +// within loop(). +// This Interrupt Service Routine is for use when the ADC is fixed timer mode. It is +// executed whenever the ADC timer expires. In this mode, the next ADC conversion is +// initiated from within this ISR. +// +void timerIsr(void) +{ + static unsigned char sample_index = 0; + + switch(sample_index) + { + case 0: + sampleV = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current + ADCSRA |= (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples. The main processor and +// the ADC work autonomously, their operation being only linked via the dataReady flag. +// As soon as a new set of data is made available by the ADC, the main processor can +// start to work on it immediately. +// +void allGeneralProcessing() +{ + static boolean triggerNeedsToBeArmed = false; // once per mains cycle (+ve half) + static int samplesDuringThisCycle; // for normalising the power in each mains cycle + static long sumP_grid; // for per-cycle summation of 'real power' + static long sumP_diverted; // for per-cycle summation of 'real power' + static enum polarities polarityOfLastSampleV; // for zero-crossing detection + static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) + static long lastSampleVminusDC_long; // for the phaseCal algorithm + static byte timerForDisplayUpdate = 0; + static enum triacStates nextStateOfTriac = TRIAC_OFF; + + // remove DC offset from the raw voltage sample by subtracting the accurate value + // as determined by a LP filter. + long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long; + + // determine the polarity of the latest voltage sample + enum polarities polarityNow; + if(sampleVminusDC_long > 0) { + polarityNow = POSITIVE; } + else { + polarityNow = NEGATIVE; } + + if (polarityNow == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + cycleCount++; + + triggerNeedsToBeArmed = true; // the trigger is armed once during each +ve half-cycle + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / samplesDuringThisCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / samplesDuringThisCycle; // proportional to Watts + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. + + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) + { + realEnergy_diverted = 0; + } + + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + if(timerForDisplayUpdate > UPDATE_PERIOD_FOR_DISPLAYED_DATA) + { // the 4-digit display needs to be refreshed every few mS. For convenience, + // this action is performed every N times around this processing loop. + timerForDisplayUpdate = 0; + +/* +// Need to comment this section out if WORKLOAD_CHECK is enabled + Serial.print("Diverted: " ); + Serial.print(divertedEnergyTotal_Wh); + Serial.print(" Wh plus "); + Serial.print((powerCal_diverted / CYCLES_PER_SECOND) * divertedEnergyRecent_IEU); + + Serial.print(" J , EDD is" ); +*/ + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + +/* +// Need to comment this section out if WORKLOAD_CHECK is enabled + if (EDD_isActive) { + Serial.println(" on" ); } + else { + Serial.println(" off" ); } +*/ + configureValueForDisplay(); + } + else + { + timerForDisplayUpdate++; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + samplesDuringThisCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + if (triggerNeedsToBeArmed == true) + { + // check to see whether the trigger device can now be reliably armed + if (samplesDuringThisCycle == 3) // much easier than checking the voltage level + { + if (energyInBucket_long < lowerEnergyThreshold_long) { + // when below the lower threshold, always set the triac to "off" + nextStateOfTriac = TRIAC_OFF; } + else + if (energyInBucket_long > upperEnergyThreshold_long) { + // when above the upper threshold, always set the triac to "off" + nextStateOfTriac = TRIAC_ON; } + else { + // otherwise, leave the triac's state unchanged (hysteresis) + } + + // set the Arduino's output pin accordingly, and clear the flag + digitalWrite(outputForTrigger, nextStateOfTriac); + triggerNeedsToBeArmed = false; + + // update the Energy Diversion Detector + if (nextStateOfTriac == TRIAC_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + } + } + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > startUpPeriod * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + samplesDuringThisCycle = 0; + Serial.println ("Go!"); + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + checkOutputModeSelection(); // updates outputMode if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is positive + + // processing for EVERY pair of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform +// long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform +// long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + samplesDuringThisCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityOfLastSampleV = polarityNow; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + + +// this function changes the value of outputMode if the state of the external switch is altered +void checkOutputModeSelection() +{ + static byte count = 0; + int pinState = digitalRead(outputModeSelectorPin); + if (pinState != outputMode) + { + count++; + } + if (count >= 20) + { + count = 0; + outputMode = (enum outputModes)pinState; // change the global variable + Serial.print ("outputMode selection changed to "); + if (outputMode == NORMAL) { + Serial.println ( "normal"); } + else { + Serial.println ( "anti-flicker"); } + + configureParamsForSelectedOutputMode(); + } +} + + +void configureParamsForSelectedOutputMode() +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + upperEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" lowerEnergyThreshold_long = "); + Serial.println(lowerEnergyThreshold_long); + Serial.print(" upperEnergyThreshold_long = "); + Serial.println(upperEnergyThreshold_long); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + +// Serial.println(divertedEnergyTotal_Wh); + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +// valueToBeDisplayed++; +} + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + // The two versions of the hardware require different logic. + +#ifdef PIN_SAVING_HARDWARE + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } + +#else // PIN_SAVING_HARDWARE + + // This version is more straightforward because the digit-enable lines can be + // used to mask out all of the transitory states, including the Decimal Point. + // The sequence is: + // + // 1. de-activate the digit-enable line that was previously active + // 2. determine the next location which is to be active + // 3. determine the relevant character for the new active location + // 4. set up the segment drivers for the character to be displayed (includes the DP) + // 5. activate the digit-enable line for the new active location + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + displayTime_count = 0; + + // 1. de-activate the location which is currently being displayed + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_DISABLED); + + // 2. determine the next digit location which is to be displayed + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 3. determine the relevant character for the new active location + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 4. set up the segment drivers for the character to be displayed (includes the DP) + for (byte segment = 0; segment < noOfSegmentsPerDigit; segment++) { + byte segmentState = segMap[digitVal][segment]; + digitalWrite( segmentDrivePin[segment], segmentState); } + + // 5. activate the digit-enable line for the new active location + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_ENABLED); + } +#endif // PIN_SAVING_HARDWARE + +} // end of refreshDisplay() + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_2.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_2.ino new file mode 100644 index 0000000..d912fa1 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_2.ino @@ -0,0 +1,1096 @@ +/* Mk2_bothDisplays_2.ino + * + * (initially released as Mk2_bothDisplays_1 in March 2014) + * This sketch is for diverting surplus PV power to a dump load using a triac. + * It is based on the Mk2i PV Router code that I have posted in on the + * OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * In this latest version, the pin-allocations have been changed to suit my + * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is + * fed from one of the secondary coils of the transformer. Current is measured + * via Current Transformers at the CT1 and CT2 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. This can be driven in two + * different ways, one with an extra pair of logic chips, and one without. The + * appropriate version of the sketch must be selected by including or commenting + * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code. + * + * September 2014: renamed as Mk2_bothDisplays_2, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - removal of unhelpful comments in the IO pin section; + * - tidier initialisation of display logic in setup(); + * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility); + * + * Robin Emley + * www.Mk2PVrouter.co.uk + * September 2014 + */ + +#include +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define SWEETZONE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +// The two versions of the hardware require different logic. The following line should +// be included if the additional logic chips are present, or excluded if they are +// absent (in which case some wire links need to be fitted) +// +//#define PIN_SAVING_HARDWARE + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum triacStates {TRIAC_ON, TRIAC_OFF}; // the external trigger device is active low +enum outputModes {ANTI_FLICKER, NORMAL}; + +// ---------------- Extra Features selection ---------------------- +// +// - WORKLOAD_CHECK, for determining how much spare processing time there is. +// +// #define WORKLOAD_CHECK // <-- Include this line is this feature is required + + +// The power-diversion logic can operate in either of two modes: +// +// - NORMAL, where the triac switches rapidly on/off to maintain a constant energy level. +// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations +// of the local mains voltage. +// +// The output mode is determined in realtime via a selector switch +enum outputModes outputMode; + +// allocation of digital pins which are not dependent on the display type that is in use +// ************************************************************************************* +const byte outputModeSelectorPin = 3; // <-- an input which uses the internal pullup +const byte outputForTrigger = 4; // <-- an output which is active-low + +// allocation of analogue pins which are not dependent on the display type that is in use +// ************************************************************************************** +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long triggerThreshold_long; // for determining when the trigger may be safely armed +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long lowerEnergyThreshold_long; // for turning triac off +long upperEnergyThreshold_long; // for turning triac on +// int phaseCal_grid_int; // to avoid the need for floating-point maths +// int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; +long mainsCyclesPerHour; + +// this setting is only used if anti-flicker mode is enabled +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.5 + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +int sampleI_grid; +int sampleI_diverted; +int sampleV; + + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// With most hardware, the default values are likely to work fine without +// need for change. A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When +// measuring a resistive load, the voltage and current waveforms should be perfectly +// aligned. In this situation, the Power Factor will be 1. +// +// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct +// value of phaseCal for any hardware configuration. An index of my various Mk2-related +// exhibits is available at http://openenergymonitor.org/emon/node/1757 +// +//const float phaseCal_grid = 1.0; <--- not used in this version +//const float phaseCal_diverted = 1.0; <--- not used in this version + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define UPDATE_PERIOD_FOR_DISPLAYED_DATA 50 // mains cycles +#define DISPLAY_SHUTDOWN_IN_HOURS 10 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +// The two versions of the hardware require different logic. +#ifdef PIN_SAVING_HARDWARE + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. In this version, +// the decimal point has to be treated differently than the other seven segments, so +// a convenient means of accessing this column is provided. +// +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + +#else // PIN_SAVING_HARDWARE + +#define ON HIGH +#define OFF LOW + +const byte noOfSegmentsPerDigit = 8; // includes one for the decimal point +enum digitEnableStates {DIGIT_ENABLED, DIGIT_DISABLED}; + +byte digitSelectorPin[noOfDigitLocations] = {16,10,13,11}; +byte segmentDrivePin[noOfSegmentsPerDigit] = {2,5,12,6,7,9,8,14}; + +// The final column of segMap[] is for the decimal point status. In this version, +// the decimal point is treated just like all the other segments, so there is +// no need to access this column specifically. +// +byte segMap[noOfPossibleCharacters][noOfSegmentsPerDigit] = { + ON , ON , ON , ON , ON , ON , OFF, OFF, // '0' <- element 0 + OFF, ON , ON , OFF, OFF, OFF, OFF, OFF, // '1' <- element 1 + ON , ON , OFF, ON , ON , OFF, ON , OFF, // '2' <- element 2 + ON , ON , ON , ON , OFF, OFF, ON , OFF, // '3' <- element 3 + OFF, ON , ON , OFF, OFF, ON , ON , OFF, // '4' <- element 4 + ON , OFF, ON , ON , OFF, ON , ON , OFF, // '5' <- element 5 + ON , OFF, ON , ON , ON , ON , ON , OFF, // '6' <- element 6 + ON , ON , ON , OFF, OFF, OFF, OFF, OFF, // '7' <- element 7 + ON , ON , ON , ON , ON , ON , ON , OFF, // '8' <- element 8 + ON , ON , ON , ON , OFF, ON , ON , OFF, // '9' <- element 9 + ON , ON , ON , ON , ON , ON , OFF, ON , // '0.' <- element 10 + OFF, ON , ON , OFF, OFF, OFF, OFF, ON , // '1.' <- element 11 + ON , ON , OFF, ON , ON , OFF, ON , ON , // '2.' <- element 12 + ON , ON , ON , ON , OFF, OFF, ON , ON , // '3.' <- element 13 + OFF, ON , ON , OFF, OFF, ON , ON , ON , // '4.' <- element 14 + ON , OFF, ON , ON , OFF, ON , ON , ON , // '5.' <- element 15 + ON , OFF, ON , ON , ON , ON , ON , ON , // '6.' <- element 16 + ON , ON , ON , OFF, OFF, OFF, OFF, ON , // '7.' <- element 17 + ON , ON , ON , ON , ON , ON , ON , ON , // '8.' <- element 18 + ON , ON , ON , ON , OFF, ON , ON , ON , // '9.' <- element 19 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, // ' ' <- element 20 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, ON // '.' <- element 11 +}; +#endif // PIN_SAVING_HARDWARE + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // energy divertion detection +long requiredExportPerMainsCycle_inIEU; + + +void setup() +{ + pinMode(outputForTrigger, OUTPUT); + digitalWrite (outputForTrigger, TRIAC_OFF); // the external trigger is active low + + pinMode(outputModeSelectorPin, INPUT); + digitalWrite(outputModeSelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(outputModeSelectorPin); // initial selection and + outputMode = (enum outputModes)pinState; // assignment of output mode + + delay(5000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_bothDisplays_2.ino"); + Serial.println(); + +#ifdef PIN_SAVING_HARDWARE + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } +#else + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + pinMode(segmentDrivePin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + pinMode(digitSelectorPin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + digitalWrite(digitSelectorPin[i], DIGIT_DISABLED); } + + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + digitalWrite(segmentDrivePin[i], OFF); } +#endif + + + + // When using integer maths, calibration values that have supplied in floating point + // form need to be rescaled. + // +// phaseCal_grid_int = phaseCal_grid * 256; // for integer maths +// phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + configureParamsForSelectedOutputMode(); + + Serial.println ("----"); + +#ifdef WORKLOAD_CHECK + Serial.println ("WELCOME TO WORKLOAD_CHECK "); + +// <<- start of commented out section, to save on RAM space! +/* + Serial.println (" This mode of operation allows the spare processing capacity of the system"); + Serial.println ("to be analysed. Additional delay is gradually increased until all spare time"); + Serial.println ("has been used up. This value (in uS) is noted and the process is repeated. "); + Serial.println ("The delay setting is increased by 1uS at a time, and each value of delay is "); + Serial.println ("checked several times before the delay is increased. "); + */ +// <<- end of commented out section, to save on RAM space! + + Serial.println (" The displayed value is the amount of spare time, per set of V & I samples, "); + Serial.println ("that is available for doing additional processing."); + Serial.println (); + #endif +} + +// An Interrupt Service Routine is now defined in which the ADC is instructed to +// measure V and I alternately. A "data ready"flag is set after each voltage conversion +// has been completed. +// For each pair of samples, this means that current is measured before voltage. The +// current sample is taken first because the phase of the waveform for current is generally +// slightly advanced relative to the waveform for voltage. The data ready flag is cleared +// within loop(). +// This Interrupt Service Routine is for use when the ADC is fixed timer mode. It is +// executed whenever the ADC timer expires. In this mode, the next ADC conversion is +// initiated from within this ISR. +// +void timerIsr(void) +{ + static unsigned char sample_index = 0; + + switch(sample_index) + { + case 0: + sampleV = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current + ADCSRA |= (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples. The main processor and +// the ADC work autonomously, their operation being only linked via the dataReady flag. +// As soon as a new set of data is made available by the ADC, the main processor can +// start to work on it immediately. +// +void allGeneralProcessing() +{ + static boolean triggerNeedsToBeArmed = false; // once per mains cycle (+ve half) + static int samplesDuringThisCycle; // for normalising the power in each mains cycle + static long sumP_grid; // for per-cycle summation of 'real power' + static long sumP_diverted; // for per-cycle summation of 'real power' + static enum polarities polarityOfLastSampleV; // for zero-crossing detection + static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) + static long lastSampleVminusDC_long; // for the phaseCal algorithm + static byte timerForDisplayUpdate = 0; + static enum triacStates nextStateOfTriac = TRIAC_OFF; + + // remove DC offset from the raw voltage sample by subtracting the accurate value + // as determined by a LP filter. + long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long; + + // determine the polarity of the latest voltage sample + enum polarities polarityNow; + if(sampleVminusDC_long > 0) { + polarityNow = POSITIVE; } + else { + polarityNow = NEGATIVE; } + + if (polarityNow == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + + triggerNeedsToBeArmed = true; // the trigger is armed once during each +ve half-cycle + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / samplesDuringThisCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / samplesDuringThisCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. + + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) + { + realEnergy_diverted = 0; + } + + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + if(timerForDisplayUpdate > UPDATE_PERIOD_FOR_DISPLAYED_DATA) + { // the 4-digit display needs to be refreshed every few mS. For convenience, + // this action is performed every N times around this processing loop. + timerForDisplayUpdate = 0; + +/* +// Need to comment this section out if WORKLOAD_CHECK is enabled + Serial.print("Diverted: " ); + Serial.print(divertedEnergyTotal_Wh); + Serial.print(" Wh plus "); + Serial.print((powerCal_diverted / CYCLES_PER_SECOND) * divertedEnergyRecent_IEU); + + Serial.print(" J , EDD is" ); +*/ + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + +/* +// Need to comment this section out if WORKLOAD_CHECK is enabled + if (EDD_isActive) { + Serial.println(" on" ); } + else { + Serial.println(" off" ); } +*/ + configureValueForDisplay(); + } + else + { + timerForDisplayUpdate++; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + samplesDuringThisCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + if (triggerNeedsToBeArmed == true) + { + // check to see whether the trigger device can now be reliably armed + if (samplesDuringThisCycle == 3) // much easier than checking the voltage level + { + if (energyInBucket_long < lowerEnergyThreshold_long) { + // when below the lower threshold, always set the triac to "off" + nextStateOfTriac = TRIAC_OFF; } + else + if (energyInBucket_long > upperEnergyThreshold_long) { + // when above the upper threshold, always set the triac to "off" + nextStateOfTriac = TRIAC_ON; } + else { + // otherwise, leave the triac's state unchanged (hysteresis) + } + + // set the Arduino's output pin accordingly, and clear the flag + digitalWrite(outputForTrigger, nextStateOfTriac); + triggerNeedsToBeArmed = false; + + // update the Energy Diversion Detector + if (nextStateOfTriac == TRIAC_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + } + } + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > startUpPeriod * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + samplesDuringThisCycle = 0; + Serial.println ("Go!"); + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + checkOutputModeSelection(); // updates outputMode if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is positive + + // processing for EVERY pair of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform +// long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform +// long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + samplesDuringThisCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityOfLastSampleV = polarityNow; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + + +// this function changes the value of outputMode if the state of the external switch is altered +void checkOutputModeSelection() +{ + static byte count = 0; + int pinState = digitalRead(outputModeSelectorPin); + if (pinState != outputMode) + { + count++; + } + if (count >= 20) + { + count = 0; + outputMode = (enum outputModes)pinState; // change the global variable + Serial.print ("outputMode selection changed to "); + if (outputMode == NORMAL) { + Serial.println ( "normal"); } + else { + Serial.println ( "anti-flicker"); } + + configureParamsForSelectedOutputMode(); + } +} + + +void configureParamsForSelectedOutputMode() +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + upperEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" lowerEnergyThreshold_long = "); + Serial.println(lowerEnergyThreshold_long); + Serial.print(" upperEnergyThreshold_long = "); + Serial.println(upperEnergyThreshold_long); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + +// Serial.println(divertedEnergyTotal_Wh); + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +// valueToBeDisplayed++; +} + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + // The two versions of the hardware require different logic. + +#ifdef PIN_SAVING_HARDWARE + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } + +#else // PIN_SAVING_HARDWARE + + // This version is more straightforward because the digit-enable lines can be + // used to mask out all of the transitory states, including the Decimal Point. + // The sequence is: + // + // 1. de-activate the digit-enable line that was previously active + // 2. determine the next location which is to be active + // 3. determine the relevant character for the new active location + // 4. set up the segment drivers for the character to be displayed (includes the DP) + // 5. activate the digit-enable line for the new active location + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + displayTime_count = 0; + + // 1. de-activate the location which is currently being displayed + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_DISABLED); + + // 2. determine the next digit location which is to be displayed + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 3. determine the relevant character for the new active location + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 4. set up the segment drivers for the character to be displayed (includes the DP) + for (byte segment = 0; segment < noOfSegmentsPerDigit; segment++) { + byte segmentState = segMap[digitVal][segment]; + digitalWrite( segmentDrivePin[segment], segmentState); } + + // 5. activate the digit-enable line for the new active location + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_ENABLED); + } +#endif // PIN_SAVING_HARDWARE + +} // end of refreshDisplay() + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_3.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_3.ino new file mode 100644 index 0000000..586b040 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_3.ino @@ -0,0 +1,1128 @@ +/* Mk2_bothDisplays_2.ino + * + * (initially released as Mk2_bothDisplays_1 in March 2014) + * This sketch is for diverting surplus PV power to a dump load using a triac. + * It is based on the Mk2i PV Router code that I have posted in on the + * OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * In this latest version, the pin-allocations have been changed to suit my + * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is + * fed from one of the secondary coils of the transformer. Current is measured + * via Current Transformers at the CT1 and CT2 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. This can be driven in two + * different ways, one with an extra pair of logic chips, and one without. The + * appropriate version of the sketch must be selected by including or commenting + * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code. + * + * September 2014: renamed as Mk2_bothDisplays_2, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - removal of unhelpful comments in the IO pin section; + * - tidier initialisation of display logic in setup(); + * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility); + * + * December 2014: renamed as Mk2_bothDisplays_3, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed) + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances + * + * Robin Emley + * www.Mk2PVrouter.co.uk + * September 2014 + */ + +#include +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define SWEETZONE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +// The two versions of the hardware require different logic. The following line should +// be included if the additional logic chips are present, or excluded if they are +// absent (in which case some wire links need to be fitted) +// +#define PIN_SAVING_HARDWARE + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum triacStates {TRIAC_ON, TRIAC_OFF}; // the external trigger device is active low +enum outputModes {ANTI_FLICKER, NORMAL}; + +// ---------------- Extra Features selection ---------------------- +// +// - WORKLOAD_CHECK, for determining how much spare processing time there is. +// +// #define WORKLOAD_CHECK // <-- Include this line is this feature is required + + +// The power-diversion logic can operate in either of two modes: +// +// - NORMAL, where the triac switches rapidly on/off to maintain a constant energy level. +// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations +// of the local mains voltage. +// +// The output mode is determined in realtime via a selector switch +enum outputModes outputMode; + +// allocation of digital pins which are not dependent on the display type that is in use +// ************************************************************************************* +const byte outputModeSelectorPin = 3; // <-- an input which uses the internal pullup +const byte outputForTrigger = 4; // <-- an output which is active-low + +// allocation of analogue pins which are not dependent on the display type that is in use +// ************************************************************************************** +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long triggerThreshold_long; // for determining when the trigger may be safely armed +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long lowerEnergyThreshold_long; // for turning triac off +long upperEnergyThreshold_long; // for turning triac on +// int phaseCal_grid_int; // to avoid the need for floating-point maths +// int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; +long mainsCyclesPerHour; + +// this setting is only used if anti-flicker mode is enabled +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.5 + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +int sampleI_grid; +int sampleI_diverted; +int sampleV; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define POLARITY_CHECK_MAXCOUNT 2 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// With most hardware, the default values are likely to work fine without +// need for change. A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When +// measuring a resistive load, the voltage and current waveforms should be perfectly +// aligned. In this situation, the Power Factor will be 1. +// +// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct +// value of phaseCal for any hardware configuration. An index of my various Mk2-related +// exhibits is available at http://openenergymonitor.org/emon/node/1757 +// +//const float phaseCal_grid = 1.0; <--- not used in this version +//const float phaseCal_diverted = 1.0; <--- not used in this version + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define UPDATE_PERIOD_FOR_DISPLAYED_DATA 50 // mains cycles +#define DISPLAY_SHUTDOWN_IN_HOURS 10 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +// The two versions of the hardware require different logic. +#ifdef PIN_SAVING_HARDWARE + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. In this version, +// the decimal point has to be treated differently than the other seven segments, so +// a convenient means of accessing this column is provided. +// +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + +#else // PIN_SAVING_HARDWARE + +#define ON HIGH +#define OFF LOW + +const byte noOfSegmentsPerDigit = 8; // includes one for the decimal point +enum digitEnableStates {DIGIT_ENABLED, DIGIT_DISABLED}; + +byte digitSelectorPin[noOfDigitLocations] = {16,10,13,11}; +byte segmentDrivePin[noOfSegmentsPerDigit] = {2,5,12,6,7,9,8,14}; + +// The final column of segMap[] is for the decimal point status. In this version, +// the decimal point is treated just like all the other segments, so there is +// no need to access this column specifically. +// +byte segMap[noOfPossibleCharacters][noOfSegmentsPerDigit] = { + ON , ON , ON , ON , ON , ON , OFF, OFF, // '0' <- element 0 + OFF, ON , ON , OFF, OFF, OFF, OFF, OFF, // '1' <- element 1 + ON , ON , OFF, ON , ON , OFF, ON , OFF, // '2' <- element 2 + ON , ON , ON , ON , OFF, OFF, ON , OFF, // '3' <- element 3 + OFF, ON , ON , OFF, OFF, ON , ON , OFF, // '4' <- element 4 + ON , OFF, ON , ON , OFF, ON , ON , OFF, // '5' <- element 5 + ON , OFF, ON , ON , ON , ON , ON , OFF, // '6' <- element 6 + ON , ON , ON , OFF, OFF, OFF, OFF, OFF, // '7' <- element 7 + ON , ON , ON , ON , ON , ON , ON , OFF, // '8' <- element 8 + ON , ON , ON , ON , OFF, ON , ON , OFF, // '9' <- element 9 + ON , ON , ON , ON , ON , ON , OFF, ON , // '0.' <- element 10 + OFF, ON , ON , OFF, OFF, OFF, OFF, ON , // '1.' <- element 11 + ON , ON , OFF, ON , ON , OFF, ON , ON , // '2.' <- element 12 + ON , ON , ON , ON , OFF, OFF, ON , ON , // '3.' <- element 13 + OFF, ON , ON , OFF, OFF, ON , ON , ON , // '4.' <- element 14 + ON , OFF, ON , ON , OFF, ON , ON , ON , // '5.' <- element 15 + ON , OFF, ON , ON , ON , ON , ON , ON , // '6.' <- element 16 + ON , ON , ON , OFF, OFF, OFF, OFF, ON , // '7.' <- element 17 + ON , ON , ON , ON , ON , ON , ON , ON , // '8.' <- element 18 + ON , ON , ON , ON , OFF, ON , ON , ON , // '9.' <- element 19 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, // ' ' <- element 20 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, ON // '.' <- element 11 +}; +#endif // PIN_SAVING_HARDWARE + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // energy divertion detection +long requiredExportPerMainsCycle_inIEU; + + +void setup() +{ + pinMode(outputForTrigger, OUTPUT); + digitalWrite (outputForTrigger, TRIAC_OFF); // the external trigger is active low + + pinMode(outputModeSelectorPin, INPUT); + digitalWrite(outputModeSelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(outputModeSelectorPin); // initial selection and + outputMode = (enum outputModes)pinState; // assignment of output mode + + delay(5000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_bothDisplays_3.ino"); + Serial.println(); + +#ifdef PIN_SAVING_HARDWARE + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } +#else + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + pinMode(segmentDrivePin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + pinMode(digitSelectorPin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + digitalWrite(digitSelectorPin[i], DIGIT_DISABLED); } + + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + digitalWrite(segmentDrivePin[i], OFF); } +#endif + + + + // When using integer maths, calibration values that have supplied in floating point + // form need to be rescaled. + // +// phaseCal_grid_int = phaseCal_grid * 256; // for integer maths +// phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples. The main processor and +// the ADC work autonomously, their operation being only linked via the dataReady flag. +// As soon as a new set of data is made available by the ADC, the main processor can +// start to work on it immediately. +// +void allGeneralProcessing() +{ + static boolean triggerNeedsToBeArmed = false; // once per mains cycle (+ve half) + static long sumP_grid; // for per-cycle summation of 'real power' + static long sumP_diverted; // for per-cycle summation of 'real power' + static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) + static long lastSampleVminusDC_long; // for the phaseCal algorithm + static byte timerForDisplayUpdate = 0; + static enum triacStates nextStateOfTriac = TRIAC_OFF; + + // remove DC offset from the raw voltage sample by subtracting the accurate value + // as determined by a LP filter. + long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long; + + // determine the polarity of the latest voltage sample + if(sampleVminusDC_long > 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + // a simple routine for checking the performance of this new ISR structure + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } + + triggerNeedsToBeArmed = true; // the trigger is armed once during each +ve half-cycle + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / sampleSetsDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. + + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) + { + realEnergy_diverted = 0; + } + + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + if(timerForDisplayUpdate > UPDATE_PERIOD_FOR_DISPLAYED_DATA) + { // the 4-digit display needs to be refreshed every few mS. For convenience, + // this action is performed every N times around this processing loop. + timerForDisplayUpdate = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); + } + else + { + timerForDisplayUpdate++; + } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + if (triggerNeedsToBeArmed == true) + { + // check to see whether the trigger device can now be reliably armed + if (sampleSetsDuringThisMainsCycle == 3) // much easier than checking the voltage level + { + if (energyInBucket_long < lowerEnergyThreshold_long) { + // when below the lower threshold, always set the triac to "off" + nextStateOfTriac = TRIAC_OFF; } + else + if (energyInBucket_long > upperEnergyThreshold_long) { + // when above the upper threshold, always set the triac to "off" + nextStateOfTriac = TRIAC_ON; } + else { + // otherwise, leave the triac's state unchanged (hysteresis) + } + + // set the Arduino's output pin accordingly, and clear the flag + digitalWrite(outputForTrigger, nextStateOfTriac); + triggerNeedsToBeArmed = false; + + // update the Energy Diversion Detector + if (nextStateOfTriac == TRIAC_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + } + } + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > startUpPeriod * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringThisMainsCycle = 0; + sampleCount_forContinuityChecker = 0; + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + checkOutputModeSelection(); // updates outputMode if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is positive + + // processing for EVERY pair of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform +// long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform +// long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + sampleSetsDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count >= POLARITY_CHECK_MAXCOUNT) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + + + +// this function changes the value of outputMode if the state of the external switch is altered +void checkOutputModeSelection() +{ + static byte count = 0; + int pinState = digitalRead(outputModeSelectorPin); + if (pinState != outputMode) + { + count++; + } + if (count >= 20) + { + count = 0; + outputMode = (enum outputModes)pinState; // change the global variable + Serial.print ("outputMode selection changed to "); + if (outputMode == NORMAL) { + Serial.println ( "normal"); } + else { + Serial.println ( "anti-flicker"); } + + configureParamsForSelectedOutputMode(); + } +} + + +void configureParamsForSelectedOutputMode() +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + upperEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" lowerEnergyThreshold_long = "); + Serial.println(lowerEnergyThreshold_long); + Serial.print(" upperEnergyThreshold_long = "); + Serial.println(upperEnergyThreshold_long); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + +// Serial.println(divertedEnergyTotal_Wh); + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +// valueToBeDisplayed++; +} + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + // The two versions of the hardware require different logic. + +#ifdef PIN_SAVING_HARDWARE + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } + +#else // PIN_SAVING_HARDWARE + + // This version is more straightforward because the digit-enable lines can be + // used to mask out all of the transitory states, including the Decimal Point. + // The sequence is: + // + // 1. de-activate the digit-enable line that was previously active + // 2. determine the next location which is to be active + // 3. determine the relevant character for the new active location + // 4. set up the segment drivers for the character to be displayed (includes the DP) + // 5. activate the digit-enable line for the new active location + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + displayTime_count = 0; + + // 1. de-activate the location which is currently being displayed + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_DISABLED); + + // 2. determine the next digit location which is to be displayed + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 3. determine the relevant character for the new active location + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 4. set up the segment drivers for the character to be displayed (includes the DP) + for (byte segment = 0; segment < noOfSegmentsPerDigit; segment++) { + byte segmentState = segMap[digitVal][segment]; + digitalWrite( segmentDrivePin[segment], segmentState); } + + // 5. activate the digit-enable line for the new active location + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_ENABLED); + } +#endif // PIN_SAVING_HARDWARE + +} // end of refreshDisplay() + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_3a.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_3a.ino new file mode 100644 index 0000000..f905283 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_3a.ino @@ -0,0 +1,1133 @@ +/* Mk2_bothDisplays_3a.ino + * + * (initially released as Mk2_bothDisplays_1 in March 2014) + * This sketch is for diverting surplus PV power to a dump load using a triac. + * It is based on the Mk2i PV Router code that I have posted in on the + * OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * In this latest version, the pin-allocations have been changed to suit my + * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is + * fed from one of the secondary coils of the transformer. Current is measured + * via Current Transformers at the CT1 and CT2 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. This can be driven in two + * different ways, one with an extra pair of logic chips, and one without. The + * appropriate version of the sketch must be selected by including or commenting + * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code. + * + * September 2014: renamed as Mk2_bothDisplays_2, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - removal of unhelpful comments in the IO pin section; + * - tidier initialisation of display logic in setup(); + * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility); + * + * December 2014: renamed as Mk2_bothDisplays_3, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed) + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances + * + * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + * December 2014 + */ + +#include +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define SWEETZONE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +// The two versions of the hardware require different logic. The following line should +// be included if the additional logic chips are present, or excluded if they are +// absent (in which case some wire links need to be fitted) +// +#define PIN_SAVING_HARDWARE + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum triacStates {TRIAC_ON, TRIAC_OFF}; // the external trigger device is active low +enum outputModes {ANTI_FLICKER, NORMAL}; + +// ---------------- Extra Features selection ---------------------- +// +// - WORKLOAD_CHECK, for determining how much spare processing time there is. +// +// #define WORKLOAD_CHECK // <-- Include this line is this feature is required + + +// The power-diversion logic can operate in either of two modes: +// +// - NORMAL, where the triac switches rapidly on/off to maintain a constant energy level. +// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations +// of the local mains voltage. +// +// The output mode is determined in realtime via a selector switch +enum outputModes outputMode; + +// allocation of digital pins which are not dependent on the display type that is in use +// ************************************************************************************* +const byte outputModeSelectorPin = 3; // <-- an input which uses the internal pullup +const byte outputForTrigger = 4; // <-- an output which is active-low + +// allocation of analogue pins which are not dependent on the display type that is in use +// ************************************************************************************** +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long triggerThreshold_long; // for determining when the trigger may be safely armed +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long lowerEnergyThreshold_long; // for turning triac off +long upperEnergyThreshold_long; // for turning triac on +// int phaseCal_grid_int; // to avoid the need for floating-point maths +// int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; +long mainsCyclesPerHour; + +// this setting is only used if anti-flicker mode is enabled +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.5 + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +int sampleI_grid; +int sampleI_diverted; +int sampleV; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define POLARITY_CHECK_MAXCOUNT 2 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// With most hardware, the default values are likely to work fine without +// need for change. A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When +// measuring a resistive load, the voltage and current waveforms should be perfectly +// aligned. In this situation, the Power Factor will be 1. +// +// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct +// value of phaseCal for any hardware configuration. An index of my various Mk2-related +// exhibits is available at http://openenergymonitor.org/emon/node/1757 +// +//const float phaseCal_grid = 1.0; <--- not used in this version +//const float phaseCal_diverted = 1.0; <--- not used in this version + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define UPDATE_PERIOD_FOR_DISPLAYED_DATA 50 // mains cycles +#define DISPLAY_SHUTDOWN_IN_HOURS 10 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +// The two versions of the hardware require different logic. +#ifdef PIN_SAVING_HARDWARE + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. In this version, +// the decimal point has to be treated differently than the other seven segments, so +// a convenient means of accessing this column is provided. +// +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + +#else // PIN_SAVING_HARDWARE + +#define ON HIGH +#define OFF LOW + +const byte noOfSegmentsPerDigit = 8; // includes one for the decimal point +enum digitEnableStates {DIGIT_ENABLED, DIGIT_DISABLED}; + +byte digitSelectorPin[noOfDigitLocations] = {16,10,13,11}; +byte segmentDrivePin[noOfSegmentsPerDigit] = {2,5,12,6,7,9,8,14}; + +// The final column of segMap[] is for the decimal point status. In this version, +// the decimal point is treated just like all the other segments, so there is +// no need to access this column specifically. +// +byte segMap[noOfPossibleCharacters][noOfSegmentsPerDigit] = { + ON , ON , ON , ON , ON , ON , OFF, OFF, // '0' <- element 0 + OFF, ON , ON , OFF, OFF, OFF, OFF, OFF, // '1' <- element 1 + ON , ON , OFF, ON , ON , OFF, ON , OFF, // '2' <- element 2 + ON , ON , ON , ON , OFF, OFF, ON , OFF, // '3' <- element 3 + OFF, ON , ON , OFF, OFF, ON , ON , OFF, // '4' <- element 4 + ON , OFF, ON , ON , OFF, ON , ON , OFF, // '5' <- element 5 + ON , OFF, ON , ON , ON , ON , ON , OFF, // '6' <- element 6 + ON , ON , ON , OFF, OFF, OFF, OFF, OFF, // '7' <- element 7 + ON , ON , ON , ON , ON , ON , ON , OFF, // '8' <- element 8 + ON , ON , ON , ON , OFF, ON , ON , OFF, // '9' <- element 9 + ON , ON , ON , ON , ON , ON , OFF, ON , // '0.' <- element 10 + OFF, ON , ON , OFF, OFF, OFF, OFF, ON , // '1.' <- element 11 + ON , ON , OFF, ON , ON , OFF, ON , ON , // '2.' <- element 12 + ON , ON , ON , ON , OFF, OFF, ON , ON , // '3.' <- element 13 + OFF, ON , ON , OFF, OFF, ON , ON , ON , // '4.' <- element 14 + ON , OFF, ON , ON , OFF, ON , ON , ON , // '5.' <- element 15 + ON , OFF, ON , ON , ON , ON , ON , ON , // '6.' <- element 16 + ON , ON , ON , OFF, OFF, OFF, OFF, ON , // '7.' <- element 17 + ON , ON , ON , ON , ON , ON , ON , ON , // '8.' <- element 18 + ON , ON , ON , ON , OFF, ON , ON , ON , // '9.' <- element 19 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, // ' ' <- element 20 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, ON // '.' <- element 11 +}; +#endif // PIN_SAVING_HARDWARE + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // energy divertion detection +long requiredExportPerMainsCycle_inIEU; + + +void setup() +{ + pinMode(outputForTrigger, OUTPUT); + digitalWrite (outputForTrigger, TRIAC_OFF); // the external trigger is active low + + pinMode(outputModeSelectorPin, INPUT); + digitalWrite(outputModeSelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(outputModeSelectorPin); // initial selection and + outputMode = (enum outputModes)pinState; // assignment of output mode + + delay(5000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_bothDisplays_3.ino"); + Serial.println(); + +#ifdef PIN_SAVING_HARDWARE + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } +#else + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + pinMode(segmentDrivePin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + pinMode(digitSelectorPin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + digitalWrite(digitSelectorPin[i], DIGIT_DISABLED); } + + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + digitalWrite(segmentDrivePin[i], OFF); } +#endif + + + + // When using integer maths, calibration values that have supplied in floating point + // form need to be rescaled. + // +// phaseCal_grid_int = phaseCal_grid * 256; // for integer maths +// phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples. The main processor and +// the ADC work autonomously, their operation being only linked via the dataReady flag. +// As soon as a new set of data is made available by the ADC, the main processor can +// start to work on it immediately. +// +void allGeneralProcessing() +{ + static boolean triggerNeedsToBeArmed = false; // once per mains cycle (+ve half) + static long sumP_grid; // for per-cycle summation of 'real power' + static long sumP_diverted; // for per-cycle summation of 'real power' + static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) + static long lastSampleVminusDC_long; // for the phaseCal algorithm + static byte timerForDisplayUpdate = 0; + static enum triacStates nextStateOfTriac = TRIAC_OFF; + + // remove DC offset from the raw voltage sample by subtracting the accurate value + // as determined by a LP filter. + long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long; + + // determine the polarity of the latest voltage sample + if(sampleVminusDC_long > 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + // a simple routine for checking the performance of this new ISR structure + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } + + triggerNeedsToBeArmed = true; // the trigger is armed once during each +ve half-cycle + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / sampleSetsDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. + + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) + { + realEnergy_diverted = 0; + } + + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + if(timerForDisplayUpdate > UPDATE_PERIOD_FOR_DISPLAYED_DATA) + { // the 4-digit display needs to be refreshed every few mS. For convenience, + // this action is performed every N times around this processing loop. + timerForDisplayUpdate = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); + } + else + { + timerForDisplayUpdate++; + } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + if (triggerNeedsToBeArmed == true) + { + // check to see whether the trigger device can now be reliably armed + if (sampleSetsDuringThisMainsCycle == 3) // much easier than checking the voltage level + { + if (energyInBucket_long < lowerEnergyThreshold_long) { + // when below the lower threshold, always set the triac to "off" + nextStateOfTriac = TRIAC_OFF; } + else + if (energyInBucket_long > upperEnergyThreshold_long) { + // when above the upper threshold, always set the triac to "off" + nextStateOfTriac = TRIAC_ON; } + else { + // otherwise, leave the triac's state unchanged (hysteresis) + } + + // set the Arduino's output pin accordingly, and clear the flag + digitalWrite(outputForTrigger, nextStateOfTriac); + triggerNeedsToBeArmed = false; + + // update the Energy Diversion Detector + if (nextStateOfTriac == TRIAC_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + } + } + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > startUpPeriod * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringThisMainsCycle = 0; + sampleCount_forContinuityChecker = 0; + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + checkOutputModeSelection(); // updates outputMode if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is positive + + // processing for EVERY set of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform +// long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform +// long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + sampleSetsDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count >= POLARITY_CHECK_MAXCOUNT) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + + + +// this function changes the value of outputMode if the state of the external switch is altered +void checkOutputModeSelection() +{ + static byte count = 0; + int pinState = digitalRead(outputModeSelectorPin); + if (pinState != outputMode) + { + count++; + } + if (count >= 20) + { + count = 0; + outputMode = (enum outputModes)pinState; // change the global variable + Serial.print ("outputMode selection changed to "); + if (outputMode == NORMAL) { + Serial.println ( "normal"); } + else { + Serial.println ( "anti-flicker"); } + + configureParamsForSelectedOutputMode(); + } +} + + +void configureParamsForSelectedOutputMode() +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + upperEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" lowerEnergyThreshold_long = "); + Serial.println(lowerEnergyThreshold_long); + Serial.print(" upperEnergyThreshold_long = "); + Serial.println(upperEnergyThreshold_long); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + +// Serial.println(divertedEnergyTotal_Wh); + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +// valueToBeDisplayed++; +} + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + // The two versions of the hardware require different logic. + +#ifdef PIN_SAVING_HARDWARE + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } + +#else // PIN_SAVING_HARDWARE + + // This version is more straightforward because the digit-enable lines can be + // used to mask out all of the transitory states, including the Decimal Point. + // The sequence is: + // + // 1. de-activate the digit-enable line that was previously active + // 2. determine the next location which is to be active + // 3. determine the relevant character for the new active location + // 4. set up the segment drivers for the character to be displayed (includes the DP) + // 5. activate the digit-enable line for the new active location + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + displayTime_count = 0; + + // 1. de-activate the location which is currently being displayed + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_DISABLED); + + // 2. determine the next digit location which is to be displayed + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 3. determine the relevant character for the new active location + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 4. set up the segment drivers for the character to be displayed (includes the DP) + for (byte segment = 0; segment < noOfSegmentsPerDigit; segment++) { + byte segmentState = segMap[digitVal][segment]; + digitalWrite( segmentDrivePin[segment], segmentState); } + + // 5. activate the digit-enable line for the new active location + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_ENABLED); + } +#endif // PIN_SAVING_HARDWARE + +} // end of refreshDisplay() + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_3b.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_3b.ino new file mode 100644 index 0000000..62f8f61 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_3b.ino @@ -0,0 +1,1141 @@ +/* Mk2_bothDisplays_3a.ino + * + * (initially released as Mk2_bothDisplays_1 in March 2014) + * This sketch is for diverting surplus PV power to a dump load using a triac. + * It is based on the Mk2i PV Router code that I have posted in on the + * OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * In this latest version, the pin-allocations have been changed to suit my + * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is + * fed from one of the secondary coils of the transformer. Current is measured + * via Current Transformers at the CT1 and CT2 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. This can be driven in two + * different ways, one with an extra pair of logic chips, and one without. The + * appropriate version of the sketch must be selected by including or commenting + * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code. + * + * September 2014: renamed as Mk2_bothDisplays_2, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - removal of unhelpful comments in the IO pin section; + * - tidier initialisation of display logic in setup(); + * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility); + * + * December 2014: renamed as Mk2_bothDisplays_3, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed) + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances + * + * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed. + * + * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to + * remove a timing uncertainty. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + * January 2016 + */ + +#include +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define SWEETZONE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +// The two versions of the hardware require different logic. The following line should +// be included if the additional logic chips are present, or excluded if they are +// absent (in which case some wire links need to be fitted) +// +#define PIN_SAVING_HARDWARE + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum triacStates {TRIAC_ON, TRIAC_OFF}; // the external trigger device is active low +enum outputModes {ANTI_FLICKER, NORMAL}; + +// ---------------- Extra Features selection ---------------------- +// +// - WORKLOAD_CHECK, for determining how much spare processing time there is. +// +// #define WORKLOAD_CHECK // <-- Include this line is this feature is required + + +// The power-diversion logic can operate in either of two modes: +// +// - NORMAL, where the triac switches rapidly on/off to maintain a constant energy level. +// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations +// of the local mains voltage. +// +// The output mode is determined in realtime via a selector switch +enum outputModes outputMode; + +// allocation of digital pins which are not dependent on the display type that is in use +// ************************************************************************************* +const byte outputModeSelectorPin = 3; // <-- an input which uses the internal pullup +const byte outputForTrigger = 4; // <-- an output which is active-low + +// allocation of analogue pins which are not dependent on the display type that is in use +// ************************************************************************************** +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long triggerThreshold_long; // for determining when the trigger may be safely armed +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long lowerEnergyThreshold_long; // for turning triac off +long upperEnergyThreshold_long; // for turning triac on +// int phaseCal_grid_int; // to avoid the need for floating-point maths +// int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; +long mainsCyclesPerHour; + +// this setting is only used if anti-flicker mode is enabled +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.5 + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +int sampleI_grid; +int sampleI_diverted; +int sampleV; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define POLARITY_CHECK_MAXCOUNT 2 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// With most hardware, the default values are likely to work fine without +// need for change. A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When +// measuring a resistive load, the voltage and current waveforms should be perfectly +// aligned. In this situation, the Power Factor will be 1. +// +// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct +// value of phaseCal for any hardware configuration. An index of my various Mk2-related +// exhibits is available at http://openenergymonitor.org/emon/node/1757 +// +//const float phaseCal_grid = 1.0; <--- not used in this version +//const float phaseCal_diverted = 1.0; <--- not used in this version + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define UPDATE_PERIOD_FOR_DISPLAYED_DATA 50 // mains cycles +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +// The two versions of the hardware require different logic. +#ifdef PIN_SAVING_HARDWARE + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. In this version, +// the decimal point has to be treated differently than the other seven segments, so +// a convenient means of accessing this column is provided. +// +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + +#else // PIN_SAVING_HARDWARE + +#define ON HIGH +#define OFF LOW + +const byte noOfSegmentsPerDigit = 8; // includes one for the decimal point +enum digitEnableStates {DIGIT_ENABLED, DIGIT_DISABLED}; + +byte digitSelectorPin[noOfDigitLocations] = {16,10,13,11}; +byte segmentDrivePin[noOfSegmentsPerDigit] = {2,5,12,6,7,9,8,14}; + +// The final column of segMap[] is for the decimal point status. In this version, +// the decimal point is treated just like all the other segments, so there is +// no need to access this column specifically. +// +byte segMap[noOfPossibleCharacters][noOfSegmentsPerDigit] = { + ON , ON , ON , ON , ON , ON , OFF, OFF, // '0' <- element 0 + OFF, ON , ON , OFF, OFF, OFF, OFF, OFF, // '1' <- element 1 + ON , ON , OFF, ON , ON , OFF, ON , OFF, // '2' <- element 2 + ON , ON , ON , ON , OFF, OFF, ON , OFF, // '3' <- element 3 + OFF, ON , ON , OFF, OFF, ON , ON , OFF, // '4' <- element 4 + ON , OFF, ON , ON , OFF, ON , ON , OFF, // '5' <- element 5 + ON , OFF, ON , ON , ON , ON , ON , OFF, // '6' <- element 6 + ON , ON , ON , OFF, OFF, OFF, OFF, OFF, // '7' <- element 7 + ON , ON , ON , ON , ON , ON , ON , OFF, // '8' <- element 8 + ON , ON , ON , ON , OFF, ON , ON , OFF, // '9' <- element 9 + ON , ON , ON , ON , ON , ON , OFF, ON , // '0.' <- element 10 + OFF, ON , ON , OFF, OFF, OFF, OFF, ON , // '1.' <- element 11 + ON , ON , OFF, ON , ON , OFF, ON , ON , // '2.' <- element 12 + ON , ON , ON , ON , OFF, OFF, ON , ON , // '3.' <- element 13 + OFF, ON , ON , OFF, OFF, ON , ON , ON , // '4.' <- element 14 + ON , OFF, ON , ON , OFF, ON , ON , ON , // '5.' <- element 15 + ON , OFF, ON , ON , ON , ON , ON , ON , // '6.' <- element 16 + ON , ON , ON , OFF, OFF, OFF, OFF, ON , // '7.' <- element 17 + ON , ON , ON , ON , ON , ON , ON , ON , // '8.' <- element 18 + ON , ON , ON , ON , OFF, ON , ON , ON , // '9.' <- element 19 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, // ' ' <- element 20 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, ON // '.' <- element 11 +}; +#endif // PIN_SAVING_HARDWARE + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // energy divertion detection +long requiredExportPerMainsCycle_inIEU; + + +void setup() +{ + pinMode(outputForTrigger, OUTPUT); + digitalWrite (outputForTrigger, TRIAC_OFF); // the external trigger is active low + + pinMode(outputModeSelectorPin, INPUT); + digitalWrite(outputModeSelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(outputModeSelectorPin); // initial selection and + outputMode = (enum outputModes)pinState; // assignment of output mode + + delay(5000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_bothDisplays_3b.ino"); + Serial.println(); + +#ifdef PIN_SAVING_HARDWARE + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } +#else + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + pinMode(segmentDrivePin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + pinMode(digitSelectorPin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + digitalWrite(digitSelectorPin[i], DIGIT_DISABLED); } + + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + digitalWrite(segmentDrivePin[i], OFF); } +#endif + + + + // When using integer maths, calibration values that have supplied in floating point + // form need to be rescaled. + // +// phaseCal_grid_int = phaseCal_grid * 256; // for integer maths +// phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples. The main processor and +// the ADC work autonomously, their operation being only linked via the dataReady flag. +// As soon as a new set of data is made available by the ADC, the main processor can +// start to work on it immediately. +// +void allGeneralProcessing() +{ + static boolean triggerNeedsToBeArmed = false; // once per mains cycle (+ve half) + static long sumP_grid; // for per-cycle summation of 'real power' + static long sumP_diverted; // for per-cycle summation of 'real power' + static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) + static long lastSampleVminusDC_long; // for the phaseCal algorithm + static byte timerForDisplayUpdate = 0; + static enum triacStates nextStateOfTriac = TRIAC_OFF; + + // remove DC offset from the raw voltage sample by subtracting the accurate value + // as determined by a LP filter. + long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long; + + // determine the polarity of the latest voltage sample + if(sampleVminusDC_long > 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + // a simple routine for checking the performance of this new ISR structure + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } + + triggerNeedsToBeArmed = true; // the trigger is armed once during each +ve half-cycle + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / sampleSetsDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. + + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) + { + realEnergy_diverted = 0; + } + + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + if(timerForDisplayUpdate > UPDATE_PERIOD_FOR_DISPLAYED_DATA) + { // the 4-digit display needs to be refreshed every few mS. For convenience, + // this action is performed every N times around this processing loop. + timerForDisplayUpdate = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); + } + else + { + timerForDisplayUpdate++; + } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + if (triggerNeedsToBeArmed == true) + { + // check to see whether the trigger device can now be reliably armed + if (sampleSetsDuringThisMainsCycle == 3) // much easier than checking the voltage level + { + if (energyInBucket_long < lowerEnergyThreshold_long) { + // when below the lower threshold, always set the triac to "off" + nextStateOfTriac = TRIAC_OFF; } + else + if (energyInBucket_long > upperEnergyThreshold_long) { + // when above the upper threshold, always set the triac to "off" + nextStateOfTriac = TRIAC_ON; } + else { + // otherwise, leave the triac's state unchanged (hysteresis) + } + + // set the Arduino's output pin accordingly, and clear the flag + digitalWrite(outputForTrigger, nextStateOfTriac); + triggerNeedsToBeArmed = false; + + // update the Energy Diversion Detector + if (nextStateOfTriac == TRIAC_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + } + } + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > startUpPeriod * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringThisMainsCycle = 0; + sampleCount_forContinuityChecker = 0; + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + checkOutputModeSelection(); // updates outputMode if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is positive + + // processing for EVERY set of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform +// long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform +// long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + sampleSetsDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count >= POLARITY_CHECK_MAXCOUNT) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + + + +// this function changes the value of outputMode if the state of the external switch is altered +void checkOutputModeSelection() +{ + static byte count = 0; + int pinState = digitalRead(outputModeSelectorPin); + if (pinState != outputMode) + { + count++; + } + if (count >= 20) + { + count = 0; + outputMode = (enum outputModes)pinState; // change the global variable + Serial.print ("outputMode selection changed to "); + if (outputMode == NORMAL) { + Serial.println ( "normal"); } + else { + Serial.println ( "anti-flicker"); } + + configureParamsForSelectedOutputMode(); + } +} + + +void configureParamsForSelectedOutputMode() +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + upperEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" lowerEnergyThreshold_long = "); + Serial.println(lowerEnergyThreshold_long); + Serial.print(" upperEnergyThreshold_long = "); + Serial.println(upperEnergyThreshold_long); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + +// Serial.println(divertedEnergyTotal_Wh); + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +// valueToBeDisplayed++; +} + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + // The two versions of the hardware require different logic. + +#ifdef PIN_SAVING_HARDWARE + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } + +#else // PIN_SAVING_HARDWARE + + // This version is more straightforward because the digit-enable lines can be + // used to mask out all of the transitory states, including the Decimal Point. + // The sequence is: + // + // 1. de-activate the digit-enable line that was previously active + // 2. determine the next location which is to be active + // 3. determine the relevant character for the new active location + // 4. set up the segment drivers for the character to be displayed (includes the DP) + // 5. activate the digit-enable line for the new active location + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + displayTime_count = 0; + + // 1. de-activate the location which is currently being displayed + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_DISABLED); + + // 2. determine the next digit location which is to be displayed + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 3. determine the relevant character for the new active location + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 4. set up the segment drivers for the character to be displayed (includes the DP) + for (byte segment = 0; segment < noOfSegmentsPerDigit; segment++) { + byte segmentState = segMap[digitVal][segment]; + digitalWrite( segmentDrivePin[segment], segmentState); } + + // 5. activate the digit-enable line for the new active location + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_ENABLED); + } +#endif // PIN_SAVING_HARDWARE + +} // end of refreshDisplay() + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_3c.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_3c.ino new file mode 100644 index 0000000..f273986 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_3c.ino @@ -0,0 +1,1144 @@ +/* Mk2_bothDisplays_3c.ino + * + * (initially released as Mk2_bothDisplays_1 in March 2014) + * This sketch is for diverting surplus PV power to a dump load using a triac. + * It is based on the Mk2i PV Router code that I have posted in on the + * OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * In this latest version, the pin-allocations have been changed to suit my + * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is + * fed from one of the secondary coils of the transformer. Current is measured + * via Current Transformers at the CT1 and CT2 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. This can be driven in two + * different ways, one with an extra pair of logic chips, and one without. The + * appropriate version of the sketch must be selected by including or commenting + * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code. + * + * September 2014: renamed as Mk2_bothDisplays_2, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - removal of unhelpful comments in the IO pin section; + * - tidier initialisation of display logic in setup(); + * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility); + * + * December 2014: renamed as Mk2_bothDisplays_3, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed) + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances + * + * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed. + * + * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to + * remove a timing uncertainty. + * + * January 2016: updated to Mk2_bothDisplays_3c: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define SWEETZONE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +// The two versions of the hardware require different logic. The following line should +// be included if the additional logic chips are present, or excluded if they are +// absent (in which case some wire links need to be fitted) +// +#define PIN_SAVING_HARDWARE + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum triacStates {TRIAC_ON, TRIAC_OFF}; // the external trigger device is active low +enum outputModes {ANTI_FLICKER, NORMAL}; + +// ---------------- Extra Features selection ---------------------- +// +// - WORKLOAD_CHECK, for determining how much spare processing time there is. +// +// #define WORKLOAD_CHECK // <-- Include this line is this feature is required + + +// The power-diversion logic can operate in either of two modes: +// +// - NORMAL, where the triac switches rapidly on/off to maintain a constant energy level. +// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations +// of the local mains voltage. +// +// The output mode is determined in realtime via a selector switch +enum outputModes outputMode; + +// allocation of digital pins which are not dependent on the display type that is in use +// ************************************************************************************* +const byte outputModeSelectorPin = 3; // <-- an input which uses the internal pullup +const byte outputForTrigger = 4; // <-- an output which is active-low + +// allocation of analogue pins which are not dependent on the display type that is in use +// ************************************************************************************** +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long triggerThreshold_long; // for determining when the trigger may be safely armed +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long lowerEnergyThreshold_long; // for turning triac off +long upperEnergyThreshold_long; // for turning triac on +// int phaseCal_grid_int; // to avoid the need for floating-point maths +// int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; +long mainsCyclesPerHour; + +// this setting is only used if anti-flicker mode is enabled +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.5 + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +volatile int sampleI_grid; +volatile int sampleI_diverted; +volatile int sampleV; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define POLARITY_CHECK_MAXCOUNT 2 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// With most hardware, the default values are likely to work fine without +// need for change. A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When +// measuring a resistive load, the voltage and current waveforms should be perfectly +// aligned. In this situation, the Power Factor will be 1. +// +// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct +// value of phaseCal for any hardware configuration. An index of my various Mk2-related +// exhibits is available at http://openenergymonitor.org/emon/node/1757 +// +//const float phaseCal_grid = 1.0; <--- not used in this version +//const float phaseCal_diverted = 1.0; <--- not used in this version + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define UPDATE_PERIOD_FOR_DISPLAYED_DATA 50 // mains cycles +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +// The two versions of the hardware require different logic. +#ifdef PIN_SAVING_HARDWARE + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. In this version, +// the decimal point has to be treated differently than the other seven segments, so +// a convenient means of accessing this column is provided. +// +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + +#else // PIN_SAVING_HARDWARE + +#define ON HIGH +#define OFF LOW + +const byte noOfSegmentsPerDigit = 8; // includes one for the decimal point +enum digitEnableStates {DIGIT_ENABLED, DIGIT_DISABLED}; + +byte digitSelectorPin[noOfDigitLocations] = {16,10,13,11}; +byte segmentDrivePin[noOfSegmentsPerDigit] = {2,5,12,6,7,9,8,14}; + +// The final column of segMap[] is for the decimal point status. In this version, +// the decimal point is treated just like all the other segments, so there is +// no need to access this column specifically. +// +byte segMap[noOfPossibleCharacters][noOfSegmentsPerDigit] = { + ON , ON , ON , ON , ON , ON , OFF, OFF, // '0' <- element 0 + OFF, ON , ON , OFF, OFF, OFF, OFF, OFF, // '1' <- element 1 + ON , ON , OFF, ON , ON , OFF, ON , OFF, // '2' <- element 2 + ON , ON , ON , ON , OFF, OFF, ON , OFF, // '3' <- element 3 + OFF, ON , ON , OFF, OFF, ON , ON , OFF, // '4' <- element 4 + ON , OFF, ON , ON , OFF, ON , ON , OFF, // '5' <- element 5 + ON , OFF, ON , ON , ON , ON , ON , OFF, // '6' <- element 6 + ON , ON , ON , OFF, OFF, OFF, OFF, OFF, // '7' <- element 7 + ON , ON , ON , ON , ON , ON , ON , OFF, // '8' <- element 8 + ON , ON , ON , ON , OFF, ON , ON , OFF, // '9' <- element 9 + ON , ON , ON , ON , ON , ON , OFF, ON , // '0.' <- element 10 + OFF, ON , ON , OFF, OFF, OFF, OFF, ON , // '1.' <- element 11 + ON , ON , OFF, ON , ON , OFF, ON , ON , // '2.' <- element 12 + ON , ON , ON , ON , OFF, OFF, ON , ON , // '3.' <- element 13 + OFF, ON , ON , OFF, OFF, ON , ON , ON , // '4.' <- element 14 + ON , OFF, ON , ON , OFF, ON , ON , ON , // '5.' <- element 15 + ON , OFF, ON , ON , ON , ON , ON , ON , // '6.' <- element 16 + ON , ON , ON , OFF, OFF, OFF, OFF, ON , // '7.' <- element 17 + ON , ON , ON , ON , ON , ON , ON , ON , // '8.' <- element 18 + ON , ON , ON , ON , OFF, ON , ON , ON , // '9.' <- element 19 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, // ' ' <- element 20 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, ON // '.' <- element 11 +}; +#endif // PIN_SAVING_HARDWARE + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // energy divertion detection +long requiredExportPerMainsCycle_inIEU; + + +void setup() +{ + pinMode(outputForTrigger, OUTPUT); + digitalWrite (outputForTrigger, TRIAC_OFF); // the external trigger is active low + + pinMode(outputModeSelectorPin, INPUT); + digitalWrite(outputModeSelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(outputModeSelectorPin); // initial selection and + outputMode = (enum outputModes)pinState; // assignment of output mode + + delay(5000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_bothDisplays_3c.ino"); + Serial.println(); + +#ifdef PIN_SAVING_HARDWARE + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } +#else + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + pinMode(segmentDrivePin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + pinMode(digitSelectorPin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + digitalWrite(digitSelectorPin[i], DIGIT_DISABLED); } + + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + digitalWrite(segmentDrivePin[i], OFF); } +#endif + + + + // When using integer maths, calibration values that have supplied in floating point + // form need to be rescaled. + // +// phaseCal_grid_int = phaseCal_grid * 256; // for integer maths +// phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples. The main processor and +// the ADC work autonomously, their operation being only linked via the dataReady flag. +// As soon as a new set of data is made available by the ADC, the main processor can +// start to work on it immediately. +// +void allGeneralProcessing() +{ + static boolean triggerNeedsToBeArmed = false; // once per mains cycle (+ve half) + static long sumP_grid; // for per-cycle summation of 'real power' + static long sumP_diverted; // for per-cycle summation of 'real power' + static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) + static long lastSampleVminusDC_long; // for the phaseCal algorithm + static byte timerForDisplayUpdate = 0; + static enum triacStates nextStateOfTriac = TRIAC_OFF; + + // remove DC offset from the raw voltage sample by subtracting the accurate value + // as determined by a LP filter. + long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long; + + // determine the polarity of the latest voltage sample + if(sampleVminusDC_long > 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + // a simple routine for checking the performance of this new ISR structure + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } + + triggerNeedsToBeArmed = true; // the trigger is armed once during each +ve half-cycle + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / sampleSetsDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. + + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) + { + realEnergy_diverted = 0; + } + + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + if(timerForDisplayUpdate > UPDATE_PERIOD_FOR_DISPLAYED_DATA) + { // the 4-digit display needs to be refreshed every few mS. For convenience, + // this action is performed every N times around this processing loop. + timerForDisplayUpdate = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); + } + else + { + timerForDisplayUpdate++; + } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + if (triggerNeedsToBeArmed == true) + { + // check to see whether the trigger device can now be reliably armed + if (sampleSetsDuringThisMainsCycle == 3) // much easier than checking the voltage level + { + if (energyInBucket_long < lowerEnergyThreshold_long) { + // when below the lower threshold, always set the triac to "off" + nextStateOfTriac = TRIAC_OFF; } + else + if (energyInBucket_long > upperEnergyThreshold_long) { + // when above the upper threshold, always set the triac to "off" + nextStateOfTriac = TRIAC_ON; } + else { + // otherwise, leave the triac's state unchanged (hysteresis) + } + + // set the Arduino's output pin accordingly, and clear the flag + digitalWrite(outputForTrigger, nextStateOfTriac); + triggerNeedsToBeArmed = false; + + // update the Energy Diversion Detector + if (nextStateOfTriac == TRIAC_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + } + } + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > startUpPeriod * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringThisMainsCycle = 0; + sampleCount_forContinuityChecker = 0; + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + checkOutputModeSelection(); // updates outputMode if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is positive + + // processing for EVERY set of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform +// long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform +// long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + sampleSetsDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count >= POLARITY_CHECK_MAXCOUNT) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + + + +// this function changes the value of outputMode if the state of the external switch is altered +void checkOutputModeSelection() +{ + static byte count = 0; + int pinState = digitalRead(outputModeSelectorPin); + if (pinState != outputMode) + { + count++; + } + if (count >= 20) + { + count = 0; + outputMode = (enum outputModes)pinState; // change the global variable + Serial.print ("outputMode selection changed to "); + if (outputMode == NORMAL) { + Serial.println ( "normal"); } + else { + Serial.println ( "anti-flicker"); } + + configureParamsForSelectedOutputMode(); + } +} + + +void configureParamsForSelectedOutputMode() +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + upperEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" lowerEnergyThreshold_long = "); + Serial.println(lowerEnergyThreshold_long); + Serial.print(" upperEnergyThreshold_long = "); + Serial.println(upperEnergyThreshold_long); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + +// Serial.println(divertedEnergyTotal_Wh); + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +// valueToBeDisplayed++; +} + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + // The two versions of the hardware require different logic. + +#ifdef PIN_SAVING_HARDWARE + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } + +#else // PIN_SAVING_HARDWARE + + // This version is more straightforward because the digit-enable lines can be + // used to mask out all of the transitory states, including the Decimal Point. + // The sequence is: + // + // 1. de-activate the digit-enable line that was previously active + // 2. determine the next location which is to be active + // 3. determine the relevant character for the new active location + // 4. set up the segment drivers for the character to be displayed (includes the DP) + // 5. activate the digit-enable line for the new active location + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + displayTime_count = 0; + + // 1. de-activate the location which is currently being displayed + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_DISABLED); + + // 2. determine the next digit location which is to be displayed + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 3. determine the relevant character for the new active location + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 4. set up the segment drivers for the character to be displayed (includes the DP) + for (byte segment = 0; segment < noOfSegmentsPerDigit; segment++) { + byte segmentState = segMap[digitVal][segment]; + digitalWrite( segmentDrivePin[segment], segmentState); } + + // 5. activate the digit-enable line for the new active location + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_ENABLED); + } +#endif // PIN_SAVING_HARDWARE + +} // end of refreshDisplay() + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_4.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_4.ino new file mode 100644 index 0000000..93740ad --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_bothDisplays_4.ino @@ -0,0 +1,1154 @@ +/* Mk2_bothDisplays_4.ino + * + * (initially released as Mk2_bothDisplays_1 in March 2014) + * This sketch is for diverting surplus PV power to a dump load using a triac or + * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on + * the OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * In this latest version, the pin-allocations have been changed to suit my + * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is + * fed from one of the secondary coils of the transformer. Current is measured + * via Current Transformers at the CT1 and CT2 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. This can be driven in two + * different ways, one with an extra pair of logic chips, and one without. The + * appropriate version of the sketch must be selected by including or commenting + * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code. + * + * September 2014: renamed as Mk2_bothDisplays_2, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - removal of unhelpful comments in the IO pin section; + * - tidier initialisation of display logic in setup(); + * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility); + * + * December 2014: renamed as Mk2_bothDisplays_3, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed) + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances + * + * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed. + * + * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to + * remove a timing uncertainty. + * + * January 2016: updated to Mk2_bothDisplays_3c: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * February 2016: updated to Mk2_bothDisplays_4, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define WORKING_RANGE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +// The two versions of the hardware require different logic. The following line should +// be included if the additional logic chips are present, or excluded if they are +// absent (in which case some wire links need to be fitted) +// +//#define PIN_SAVING_HARDWARE + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum loadStates {LOAD_ON, LOAD_OFF}; // the external trigger device is active low +enum outputModes {ANTI_FLICKER, NORMAL}; + +// ---------------- Extra Features selection ---------------------- +// +// - WORKLOAD_CHECK, for determining how much spare processing time there is. +// +// #define WORKLOAD_CHECK // <-- Include this line is this feature is required + + +// The power-diversion logic can operate in either of two modes: +// +// - NORMAL, where the load switches rapidly on/off to maintain a constant energy level. +// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations +// of the local mains voltage. +// +// The output mode is determined in realtime via a selector switch +enum outputModes outputMode; + +// allocation of digital pins which are not dependent on the display type that is in use +// ************************************************************************************* +const byte outputModeSelectorPin = 3; // <-- an input which uses the internal pullup +const byte outputForTrigger = 4; // <-- an output which is active-low + +// allocation of analogue pins which are not dependent on the display type that is in use +// ************************************************************************************** +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + +const byte delayBeforeSerialStarts = 3; // in seconds, to allow Serial window to be opened +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long triggerThreshold_long; // for determining when the trigger may be safely armed +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long lowerEnergyThreshold_long; // for turning load off +long upperEnergyThreshold_long; // for turning load on +// int phaseCal_grid_int; // to avoid the need for floating-point maths +// int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; +long mainsCyclesPerHour; + +// this setting is only used if anti-flicker mode is enabled +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.5 + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +volatile int sampleI_grid; +volatile int sampleI_diverted; +volatile int sampleV; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define PERSISTENCE_FOR_POLARITY_CHANGE 1 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// With most hardware, the default values are likely to work fine without +// need for change. A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When +// measuring a resistive load, the voltage and current waveforms should be perfectly +// aligned. In this situation, the Power Factor will be 1. +// +// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct +// value of phaseCal for any hardware configuration. An index of my various Mk2-related +// exhibits is available at http://openenergymonitor.org/emon/node/1757 +// +//const float phaseCal_grid = 1.0; <--- not used in this version +//const float phaseCal_diverted = 1.0; <--- not used in this version + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define UPDATE_PERIOD_FOR_DISPLAYED_DATA 50 // mains cycles +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +// The two versions of the hardware require different logic. +#ifdef PIN_SAVING_HARDWARE + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. In this version, +// the decimal point has to be treated differently than the other seven segments, so +// a convenient means of accessing this column is provided. +// +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + +#else // PIN_SAVING_HARDWARE + +#define ON HIGH +#define OFF LOW + +const byte noOfSegmentsPerDigit = 8; // includes one for the decimal point +enum digitEnableStates {DIGIT_ENABLED, DIGIT_DISABLED}; + +byte digitSelectorPin[noOfDigitLocations] = {16,10,13,11}; +byte segmentDrivePin[noOfSegmentsPerDigit] = {2,5,12,6,7,9,8,14}; + +// The final column of segMap[] is for the decimal point status. In this version, +// the decimal point is treated just like all the other segments, so there is +// no need to access this column specifically. +// +byte segMap[noOfPossibleCharacters][noOfSegmentsPerDigit] = { + ON , ON , ON , ON , ON , ON , OFF, OFF, // '0' <- element 0 + OFF, ON , ON , OFF, OFF, OFF, OFF, OFF, // '1' <- element 1 + ON , ON , OFF, ON , ON , OFF, ON , OFF, // '2' <- element 2 + ON , ON , ON , ON , OFF, OFF, ON , OFF, // '3' <- element 3 + OFF, ON , ON , OFF, OFF, ON , ON , OFF, // '4' <- element 4 + ON , OFF, ON , ON , OFF, ON , ON , OFF, // '5' <- element 5 + ON , OFF, ON , ON , ON , ON , ON , OFF, // '6' <- element 6 + ON , ON , ON , OFF, OFF, OFF, OFF, OFF, // '7' <- element 7 + ON , ON , ON , ON , ON , ON , ON , OFF, // '8' <- element 8 + ON , ON , ON , ON , OFF, ON , ON , OFF, // '9' <- element 9 + ON , ON , ON , ON , ON , ON , OFF, ON , // '0.' <- element 10 + OFF, ON , ON , OFF, OFF, OFF, OFF, ON , // '1.' <- element 11 + ON , ON , OFF, ON , ON , OFF, ON , ON , // '2.' <- element 12 + ON , ON , ON , ON , OFF, OFF, ON , ON , // '3.' <- element 13 + OFF, ON , ON , OFF, OFF, ON , ON , ON , // '4.' <- element 14 + ON , OFF, ON , ON , OFF, ON , ON , ON , // '5.' <- element 15 + ON , OFF, ON , ON , ON , ON , ON , ON , // '6.' <- element 16 + ON , ON , ON , OFF, OFF, OFF, OFF, ON , // '7.' <- element 17 + ON , ON , ON , ON , ON , ON , ON , ON , // '8.' <- element 18 + ON , ON , ON , ON , OFF, ON , ON , ON , // '9.' <- element 19 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, // ' ' <- element 20 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, ON // '.' <- element 11 +}; +#endif // PIN_SAVING_HARDWARE + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // energy divertion detection +long requiredExportPerMainsCycle_inIEU; + + +void setup() +{ + pinMode(outputForTrigger, OUTPUT); + digitalWrite (outputForTrigger, LOAD_OFF); // the external trigger is active low + + pinMode(outputModeSelectorPin, INPUT); + digitalWrite(outputModeSelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(outputModeSelectorPin); // initial selection and + outputMode = (enum outputModes)pinState; // assignment of output mode + + delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_bothDisplays_4.ino"); + Serial.println(); + +#ifdef PIN_SAVING_HARDWARE + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } +#else + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + pinMode(segmentDrivePin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + pinMode(digitSelectorPin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + digitalWrite(digitSelectorPin[i], DIGIT_DISABLED); } + + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + digitalWrite(segmentDrivePin[i], OFF); } +#endif + + + + // When using integer maths, calibration values that have supplied in floating point + // form need to be rescaled. + // +// phaseCal_grid_int = phaseCal_grid * 256; // for integer maths +// phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + energyInBucket_long = 0; + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples. The main processor and +// the ADC work autonomously, their operation being only linked via the dataReady flag. +// As soon as a new set of data is made available by the ADC, the main processor can +// start to work on it immediately. +// +void allGeneralProcessing() +{ + static long sumP_grid; // for per-cycle summation of 'real power' + static long sumP_diverted; // for per-cycle summation of 'real power' + static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) + static long lastSampleVminusDC_long; // for the phaseCal algorithm + static byte timerForDisplayUpdate = 0; + static enum loadStates nextStateOfLoad = LOAD_OFF; + + // remove DC offset from the raw voltage sample by subtracting the accurate value + // as determined by a LP filter. + long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long; + + // determine the polarity of the latest voltage sample + if(sampleVminusDC_long > 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + if (beyondStartUpPhase) + { + // a simple routine for checking the performance of this new ISR structure + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / sampleSetsDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. + + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) + { + realEnergy_diverted = 0; + } + + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + if(timerForDisplayUpdate > UPDATE_PERIOD_FOR_DISPLAYED_DATA) + { // the 4-digit display needs to be refreshed every few mS. For convenience, + // this action is performed every N times around this processing loop. + timerForDisplayUpdate = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); + } + else + { + timerForDisplayUpdate++; + } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringThisMainsCycle = 0; // not yet dealt with for this cycle + sampleCount_forContinuityChecker = 1; // opportunity has been missed for this cycle + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + // check to see whether the trigger device can now be reliably armed + if (sampleSetsDuringThisMainsCycle == 3) // much easier than checking the voltage level + { + if (beyondStartUpPhase) + { + if (energyInBucket_long < lowerEnergyThreshold_long) { + // when below the lower threshold, always set the load to "off" + nextStateOfLoad = LOAD_OFF; } + else + if (energyInBucket_long > upperEnergyThreshold_long) { + // when above the upper threshold, always set the load to "off" + nextStateOfLoad = LOAD_ON; } + else { + // otherwise, leave the load's state unchanged (hysteresis) + } + + // set the Arduino's output pin accordingly, and clear the flag + digitalWrite(outputForTrigger, nextStateOfLoad); + + // update the Energy Diversion Detector + if (nextStateOfLoad == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + } + } + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12); + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + checkOutputModeSelection(); // updates outputMode if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is negative + + // processing for EVERY set of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform +// long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform +// long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + sampleSetsDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count > PERSISTENCE_FOR_POLARITY_CHANGE) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + + + +// this function changes the value of outputMode if the state of the external switch is altered +void checkOutputModeSelection() +{ + static byte count = 0; + int pinState = digitalRead(outputModeSelectorPin); + if (pinState != outputMode) + { + count++; + } + if (count >= 20) + { + count = 0; + outputMode = (enum outputModes)pinState; // change the global variable + Serial.print ("outputMode selection changed to "); + if (outputMode == NORMAL) { + Serial.println ( "normal"); } + else { + Serial.println ( "anti-flicker"); } + + configureParamsForSelectedOutputMode(); + } +} + + +void configureParamsForSelectedOutputMode() +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + upperEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" lowerEnergyThreshold_long = "); + Serial.println(lowerEnergyThreshold_long); + Serial.print(" upperEnergyThreshold_long = "); + Serial.println(upperEnergyThreshold_long); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + +// Serial.println(divertedEnergyTotal_Wh); + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +// valueToBeDisplayed++; +} + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + // The two versions of the hardware require different logic. + +#ifdef PIN_SAVING_HARDWARE + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } + +#else // PIN_SAVING_HARDWARE + + // This version is more straightforward because the digit-enable lines can be + // used to mask out all of the transitory states, including the Decimal Point. + // The sequence is: + // + // 1. de-activate the digit-enable line that was previously active + // 2. determine the next location which is to be active + // 3. determine the relevant character for the new active location + // 4. set up the segment drivers for the character to be displayed (includes the DP) + // 5. activate the digit-enable line for the new active location + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + displayTime_count = 0; + + // 1. de-activate the location which is currently being displayed + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_DISABLED); + + // 2. determine the next digit location which is to be displayed + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 3. determine the relevant character for the new active location + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 4. set up the segment drivers for the character to be displayed (includes the DP) + for (byte segment = 0; segment < noOfSegmentsPerDigit; segment++) { + byte segmentState = segMap[digitVal][segment]; + digitalWrite( segmentDrivePin[segment], segmentState); } + + // 5. activate the digit-enable line for the new active location + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_ENABLED); + } +#endif // PIN_SAVING_HARDWARE + +} // end of refreshDisplay() + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_datalog_results_2.txt b/docs/routers/mk2pvrouter.co.uk/Mk2_datalog_results_2.txt new file mode 100644 index 0000000..737a92d --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_datalog_results_2.txt @@ -0,0 +1,141 @@ +Mk2_datalog_results_2.txt + +Initial results for a revised version of my +Mk2 PV Router sketch which includes datalogging. + +Sketch ID: Mk2_RFdatalog_1.ino + +This sketch was run on one of the PCBs that I can supply from www.mk2pvrouter.co.uk. Data was transmitted and received via +a pair of 433 MHz RFM12B modules and displayed via the +Serial Monitor at the receiver board. All data fields are +integers, the payload structure is: + +typedef struct { + int msgNumber; + int powerAtSupplyPoint; + int divertedEnergyTotal; +} Tx_struct; +Tx_struct tx_data; + +Messages are transmitted and received every 2 seconds. This +data has been captured from a 'live' installation when approx +500 Watts of surplus power was available for diversion. The +data below shows how the PV Router is constantly adjusting the +overall consumption to match generation. The net flow of +current, as shown in the second column, is therefore zero. + +The sequence below was as follows: + +At T=225, when datalogging was started, approx 500W of surplus power was flowing out to the grid. The total amount of energy that had been already sent to my 3 kW kettle during this session was 74 Wh (i.e. 0.074 kWh). + +At T=240, I turned my 3 kW kettle 'on'. With the router's energy bucket already being full to maximum, there was an inrush of power (904 Watts, averaged over 2 seconds), after which the system settled down into its 'normal' operating mode. During the next 40 seconds (20 counts), the accumulated total slowly increased, but the average energy flow at the supply point was tightly constrained to be close to zero. This is a nice demonstration of the router's "normal" mode. + +At T=259, I turned the kettle 'off'. By this time, surplus power had dropped to 400 Watts. The total amount of diverted energy was then 80 Wh. + +At T=267, I turned the kettle back on again with the router having been switched into its anti-flicker mode. As the router's energy bucket was again full to maximum, there was another inrush of power (920 Watts, averaged over 2 seconds), after which the system settled down into its 'anti-flicker' operating mode. + +During the next 50 seconds (25 counts), the accumulated total continued to increase, and the average energy flow at the supply point was again held close to zero. Because the on and off periods in "anti-flicker" mode are much longer, the individual power values are the supply point are correspondingly greater. Over any reasonable timescale, I am confident that they will average out to zero. During this phase, another 5 WattHours worth of energy was added to the total. + +At T=292, the kettle was finally turned off. By this time, the amount of surplus PV was dropping rapidly as our panels become more shaded. + +Because these data values are all transmitted as integers, they are directly suitable for processing by emonCMS for further analysis and storage + +The "corrupt message" events may have been due to my poor aerials. These were just two lengths of wire, one of which was resting in the hole on the PCB, not even soldered in place. The transmitter and receiver were on opposite sides of an external brick wall. + +Robin Emley +mk2PVrouter.co.uk + +18/04/2014 + + + +225, 516, 74 +226, 515, 74 +227, 512, 74 +228, 509, 74 +229, 506, 74 +230, 503, 74 +231, 499, 74 +232, 497, 74 +233, 495, 74 +234, 491, 74 +235, 488, 74 +Corrupt message! +236, 481, 74 +237, 481, 74 +238, 476, 74 +239, 476, 74 +240, -904, 75 +241, 9, 75 +242, 3, 76 +243, 4, 76 +244, -3, 76 +245, 0, 76 +Corrupt message! +246, -3, 77 +247, -5, 77 +248, 8, 77 +249, 8, 77 +250, -16, 78 +251, 5, 78 +Corrupt message! +252, 3, 78 +253, 0, 78 +254, -5, 79 +255, -9, 79 +Message numbering error! +256, -11, 79 +Corrupt message! +257, 3, 79 +258, 8, 80 +259, 344, 80 +260, 413, 80 +261, 410, 80 +262, 407, 80 +263, 404, 80 +264, 401, 80 +265, 398, 80 +266, 394, 80 +267, -920, 81 +268, -69, 81 +269, -73, 81 +270, 171, 81 +271, 170, 81 +272, -79, 82 +273, -84, 82 +274, -92, 82 +275, -91, 82 +276, 363, 82 +277, -96, 83 +Corrupt message! +278, -100, 83 +279, -103, 83 +280, 50, 83 +281, 224, 83 +282, -113, 84 +283, -118, 84 +284, -99, 84 +285, 329, 84 +286, -104, 84 +287, -103, 85 +288, -105, 85 +289, 320, 85 +290, -111, 85 +291, 312, 85 +Corrupt message! +292, 308, 85 +293, 306, 85 +294, 302, 85 +Corrupt message! +295, 299, 85 +296, 297, 85 +297, 293, 85 +Corrupt message! +298, 289, 85 +299, 286, 85 +300, 281, 85 +301, 277, 85 +302, 277, 85 +303, 272, 85 +304, 269, 85 +305, 267, 85 diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_1.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_1.ino new file mode 100644 index 0000000..80a24eb --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_1.ino @@ -0,0 +1,997 @@ +/* Mk2_fasterControl_1.ino + * + * (initially released as Mk2_bothDisplays_1 in March 2014) + * This sketch is for diverting suplus PV power to a dump load using a triac or + * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on + * the OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * In this latest version, the pin-allocations have been changed to suit my + * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is + * fed from one of the secondary coils of the transformer. Current is measured + * via Current Transformers at the CT1 and CT1 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. This can be driven in two + * different ways, one with an extra pair of logic chips, and one without. The + * appropriate version of the sketch must be selected by including or commenting + * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code. + * + * September 2014: renamed as Mk2_bothDisplays_2, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - removal of unhelpful comments in the IO pin section; + * - tidier initialisation of display logic in setup(); + * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility); + * + * December 2014: renamed as Mk2_bothDisplays_3, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed) + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances + * + * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed. + * + * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to + * remove a timing uncertainty. + * + * January 2016: updated to Mk2_bothDisplays_3c: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * February 2016: updated to Mk2_bothDisplays_4, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * November 2019: updated to Mk2_fasterControl_1 with these changes: + * - Half way through each mains cycle, a prediction is made of the likely energy level at the + * end of the cycle. That predicted value allows the triac to be switched at the +ve going + * zero-crossing point rather than waiting for a further 10 ms. These changes allow for + * faster switching of the load. + * - The range of the energy bucket has been reduced to one tenth of its former value. This + * allows the unit's operation to commence more rapidly whenever surplus power is available. + * - controlMode is no longer selectable, the unit's operation being effectively hard-coded + * as "Normal" rather than Anti-flicker. + * - Port D3 now supports an indicator which shows when the level in the energy bucket + * reaches either end of its range. While the unit is actively diverting surplus power, + * it is vital that the level in the reduced capacity energy bucket remains within its + * permitted range, hence the addition of this indicator. + * + * * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define WORKING_RANGE_IN_JOULES 360 // 0.1 Wh, reduced for faster start-up +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +// The two versions of the hardware require different logic. The following line should +// be included if the additional logic chips are present, or excluded if they are +// absent (in which case some wire links need to be fitted) +// +#define PIN_SAVING_HARDWARE + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum ledStates {LED_OFF, LED_ON}; // for use at port D3 which is active-high +enum loadStates {LOAD_ON, LOAD_OFF}; // for use at port D4 which is active-low + +// For this go-faster version, the unit's operation will effectively always be "Normal"; +// there is no "Anti-flicker" option. The controlMode variable has been removed. + +// allocation of digital pins which are not dependent on the display type that is in use +// ************************************************************************************* +const byte outOfRangeIndication = 3; // <-- this output port is active-high +const byte outputForTrigger = 4; // <-- this output port is active-low + +// allocation of analogue pins which are not dependent on the display type that is in use +// ************************************************************************************** +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + +const byte delayBeforeSerialStarts = 1; // in seconds, to allow Serial window to be opened +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long singleEnergyThreshold_long; // for go-faster use + +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; +long mainsCyclesPerHour; + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +volatile int sampleI_grid; +volatile int sampleI_diverted; +volatile int sampleV; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define PERSISTENCE_FOR_POLARITY_CHANGE 1 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "RawSamplesTool_2chan.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + +// for this go-faster sketch, the phaseCal logic has been removed. If required, it can be +// found in most of the standard Mk2_bothDisplay_n versions + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define UPDATE_PERIOD_FOR_DISPLAYED_DATA 50 // mains cycles +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +// The two versions of the hardware require different logic. +#ifdef PIN_SAVING_HARDWARE + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. In this version, +// the decimal point has to be treated differently than the other seven segments, so +// a convenient means of accessing this column is provided. +// +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + +#else // PIN_SAVING_HARDWARE + +#define ON HIGH +#define OFF LOW + +const byte noOfSegmentsPerDigit = 8; // includes one for the decimal point +enum digitEnableStates {DIGIT_ENABLED, DIGIT_DISABLED}; + +byte digitSelectorPin[noOfDigitLocations] = {16,10,13,11}; +byte segmentDrivePin[noOfSegmentsPerDigit] = {2,5,12,6,7,9,8,14}; + +// The final column of segMap[] is for the decimal point status. In this version, +// the decimal point is treated just like all the other segments, so there is +// no need to access this column specifically. +// +byte segMap[noOfPossibleCharacters][noOfSegmentsPerDigit] = { + ON , ON , ON , ON , ON , ON , OFF, OFF, // '0' <- element 0 + OFF, ON , ON , OFF, OFF, OFF, OFF, OFF, // '1' <- element 1 + ON , ON , OFF, ON , ON , OFF, ON , OFF, // '2' <- element 2 + ON , ON , ON , ON , OFF, OFF, ON , OFF, // '3' <- element 3 + OFF, ON , ON , OFF, OFF, ON , ON , OFF, // '4' <- element 4 + ON , OFF, ON , ON , OFF, ON , ON , OFF, // '5' <- element 5 + ON , OFF, ON , ON , ON , ON , ON , OFF, // '6' <- element 6 + ON , ON , ON , OFF, OFF, OFF, OFF, OFF, // '7' <- element 7 + ON , ON , ON , ON , ON , ON , ON , OFF, // '8' <- element 8 + ON , ON , ON , ON , OFF, ON , ON , OFF, // '9' <- element 9 + ON , ON , ON , ON , ON , ON , OFF, ON , // '0.' <- element 10 + OFF, ON , ON , OFF, OFF, OFF, OFF, ON , // '1.' <- element 11 + ON , ON , OFF, ON , ON , OFF, ON , ON , // '2.' <- element 12 + ON , ON , ON , ON , OFF, OFF, ON , ON , // '3.' <- element 13 + OFF, ON , ON , OFF, OFF, ON , ON , ON , // '4.' <- element 14 + ON , OFF, ON , ON , OFF, ON , ON , ON , // '5.' <- element 15 + ON , OFF, ON , ON , ON , ON , ON , ON , // '6.' <- element 16 + ON , ON , ON , OFF, OFF, OFF, OFF, ON , // '7.' <- element 17 + ON , ON , ON , ON , ON , ON , ON , ON , // '8.' <- element 18 + ON , ON , ON , ON , OFF, ON , ON , ON , // '9.' <- element 19 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, // ' ' <- element 20 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, ON // '.' <- element 11 +}; +#endif // PIN_SAVING_HARDWARE + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // energy divertion detection +long requiredExportPerMainsCycle_inIEU; + + +void setup() +{ + pinMode(outOfRangeIndication, OUTPUT); + digitalWrite (outOfRangeIndication, LED_OFF); + + pinMode(outputForTrigger, OUTPUT); + digitalWrite (outputForTrigger, LOAD_OFF); + + delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_fasterControl_1.ino"); + Serial.println(); + +#ifdef PIN_SAVING_HARDWARE + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } +#else + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + pinMode(segmentDrivePin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + pinMode(digitSelectorPin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + digitalWrite(digitSelectorPin[i], DIGIT_DISABLED); } + + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + digitalWrite(segmentDrivePin[i], OFF); } +#endif + + + + // When using integer maths, calibration values that have supplied in floating point + // form need to be rescaled. + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + energyInBucket_long = 0; + + singleEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + + Serial.println ("----"); +} + +// An Interrupt Service Routine is now defined in which the ADC is instructed to +// measure each analogue input in sequence. A "data ready" flag is set after each +// voltage conversion has been completed. +// For each set of samples, the two samples for current are taken before the one +// for voltage. This is appropriate because each waveform current is generally slightly +// advanced relative to the waveform for voltage. The data ready flag is cleared +// within loop(). +// This Interrupt Service Routine is for use when the ADC is fixed timer mode. It is +// executed whenever the ADC timer expires. In this mode, the next ADC conversion is +// initiated from within this ISR. +// +void timerIsr(void) +{ + static unsigned char sample_index = 0; + static int sampleI_grid_raw; + static int sampleI_diverted_raw; + + + switch(sample_index) + { + case 0: + sampleV = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current + ADCSRA |= (1< 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + if (beyondStartUpPhase) + { + // a simple routine for checking the performance of this new ISR structure + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / sampleSetsDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step would normally be to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient to just + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + bool endOfRangeEncountered = false; + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; + endOfRangeEncountered = true;} + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; + endOfRangeEncountered = true;} + + if (endOfRangeEncountered) { + digitalWrite (outOfRangeIndication , LED_ON); } + else { + digitalWrite (outOfRangeIndication , LED_OFF); } + + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. + + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) + { + realEnergy_diverted = 0; + } + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + if(timerForDisplayUpdate > UPDATE_PERIOD_FOR_DISPLAYED_DATA) + { // the 4-digit display needs to be refreshed every few mS. For convenience, + // this action is performed every N times around this processing loop. + timerForDisplayUpdate = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); + } + else + { + timerForDisplayUpdate++; + } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringNegativeHalfOfMainsCycle = 0; + + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringThisMainsCycle = 0; // not yet dealt with for this cycle + sampleCount_forContinuityChecker = 1; // opportunity has been missed for this cycle + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + // (in this go-faster code, the action from here has moved to the negative half of the cycle) + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12); + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + // The average power that has been measured during the first half of this mains cycle can now be used + // to predict the energy state at the end of this mains cycle. That prediction will be used to alter + // the state of the load as necessary. The arming signal for the triac can't be set yet - that must + // wait until the voltage has advanced further beyond the -ve going zero-crossing point. + // + long averagePower = sumP_grid / sampleSetsDuringThisMainsCycle;// for 1st half of this mains cycle only + // + // To avoid repetitive and unnecessary calculations, the increase in energy during each mains cycle is + // deemed to be numerically equal to the average power. The predicted value for the energy state at the + // end of this mains cycle will therefore be the known energy state at its start plus the average power + // as measured. Although the average power has been determined over only half a mains cycle, the correct + // number of contributing sample sets has been used so the result can be expected to be a true measurement + // of average power, not half of it. + // However, it can be shown that the average power during the first half of any mains cycle after the + // load has changed state will alway be under-recorded so its value should now be increased by 30%. This + // arbitrary looking adjustment gives good test results with differening amounts of surplus power and it + // only affects the predicted value of the energy state at the end of the current mains cycle; it does + // not affect the value in the main energy bucket. This complication is a fundamental limitation + // of the floating CTs that we use. + // + if (loadHasJustChangedState) + { + averagePower = averagePower * 1.3; + } + energyInBucket_prediction = energyInBucket_long + averagePower; // at end of this mains cycle + + + } // end of processing that is specific to the first Vsample in each -ve half cycle + + // check to see whether the trigger device can now be reliably armed + if (sampleSetsDuringNegativeHalfOfMainsCycle == 3) + { + if (beyondStartUpPhase) + { + enum loadStates prevStateOfLoad = nextStateOfLoad; + if (energyInBucket_prediction < singleEnergyThreshold_long) { + nextStateOfLoad = LOAD_OFF; } + else + if (energyInBucket_prediction >= singleEnergyThreshold_long) { + nextStateOfLoad = LOAD_ON; } + + if (nextStateOfLoad != prevStateOfLoad) { + loadHasJustChangedState = true; } + else { + loadHasJustChangedState = false; } + + // set the Arduino's output pin accordingly, and clear the flag + digitalWrite(outputForTrigger, nextStateOfLoad); + + // update the Energy Diversion Detector + if (nextStateOfLoad == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + } + } + + sampleSetsDuringNegativeHalfOfMainsCycle++; + } // end of processing that is specific to samples where the voltage is negative + + // processing for EVERY set of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + sampleSetsDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count > PERSISTENCE_FOR_POLARITY_CHANGE) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + +// Serial.println(divertedEnergyTotal_Wh); + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +} + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + // The two versions of the hardware require different logic. + +#ifdef PIN_SAVING_HARDWARE + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } + +#else // PIN_SAVING_HARDWARE + + // This version is more straightforward because the digit-enable lines can be + // used to mask out all of the transitory states, including the Decimal Point. + // The sequence is: + // + // 1. de-activate the digit-enable line that was previously active + // 2. determine the next location which is to be active + // 3. determine the relevant character for the new active location + // 4. set up the segment drivers for the character to be displayed (includes the DP) + // 5. activate the digit-enable line for the new active location + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + displayTime_count = 0; + + // 1. de-activate the location which is currently being displayed + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_DISABLED); + + // 2. determine the next digit location which is to be displayed + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 3. determine the relevant character for the new active location + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 4. set up the segment drivers for the character to be displayed (includes the DP) + for (byte segment = 0; segment < noOfSegmentsPerDigit; segment++) { + byte segmentState = segMap[digitVal][segment]; + digitalWrite( segmentDrivePin[segment], segmentState); } + + // 5. activate the digit-enable line for the new active location + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_ENABLED); + } +#endif // PIN_SAVING_HARDWARE + +} // end of refreshDisplay() + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_1a.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_1a.ino new file mode 100644 index 0000000..65e63c7 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_1a.ino @@ -0,0 +1,999 @@ +/* Mk2_fasterControl_1a.ino + * + * (initially released as Mk2_bothDisplays_1 in March 2014) + * This sketch is for diverting suplus PV power to a dump load using a triac or + * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on + * the OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * In this latest version, the pin-allocations have been changed to suit my + * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is + * fed from one of the secondary coils of the transformer. Current is measured + * via Current Transformers at the CT1 and CT1 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. This can be driven in two + * different ways, one with an extra pair of logic chips, and one without. The + * appropriate version of the sketch must be selected by including or commenting + * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code. + * + * September 2014: renamed as Mk2_bothDisplays_2, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - removal of unhelpful comments in the IO pin section; + * - tidier initialisation of display logic in setup(); + * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility); + * + * December 2014: renamed as Mk2_bothDisplays_3, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed) + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances + * + * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed. + * + * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to + * remove a timing uncertainty. + * + * January 2016: updated to Mk2_bothDisplays_3c: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * February 2016: updated to Mk2_bothDisplays_4, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * November 2019: updated to Mk2_fasterControl_1 with these changes: + * - Half way through each mains cycle, a prediction is made of the likely energy level at the + * end of the cycle. That predicted value allows the triac to be switched at the +ve going + * zero-crossing point rather than waiting for a further 10 ms. These changes allow for + * faster switching of the load. + * - The range of the energy bucket has been reduced to one tenth of its former value. This + * allows the unit's operation to commence more rapidly whenever surplus power is available. + * - controlMode is no longer selectable, the unit's operation being effectively hard-coded + * as "Normal" rather than Anti-flicker. + * - Port D3 now supports an indicator which shows when the level in the energy bucket + * reaches either end of its range. While the unit is actively diverting surplus power, + * it is vital that the level in the reduced capacity energy bucket remains within its + * permitted range, hence the addition of this indicator. + * + * February 2020: updated to Mk2_fasterControl_1a with these changes: + * - removal of some redundant code in the logic for determining the next load state. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define WORKING_RANGE_IN_JOULES 360 // 0.1 Wh, reduced for faster start-up +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +// The two versions of the hardware require different logic. The following line should +// be included if the additional logic chips are present, or excluded if they are +// absent (in which case some wire links need to be fitted) +// +#define PIN_SAVING_HARDWARE + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum ledStates {LED_OFF, LED_ON}; // for use at port D3 which is active-high +enum loadStates {LOAD_ON, LOAD_OFF}; // for use at port D4 which is active-low + +// For this go-faster version, the unit's operation will effectively always be "Normal"; +// there is no "Anti-flicker" option. The controlMode variable has been removed. + +// allocation of digital pins which are not dependent on the display type that is in use +// ************************************************************************************* +const byte outOfRangeIndication = 3; // <-- this output port is active-high +const byte outputForTrigger = 4; // <-- this output port is active-low + +// allocation of analogue pins which are not dependent on the display type that is in use +// ************************************************************************************** +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + +const byte delayBeforeSerialStarts = 1; // in seconds, to allow Serial window to be opened +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long singleEnergyThreshold_long; // for go-faster use + +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; +long mainsCyclesPerHour; + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +volatile int sampleI_grid; +volatile int sampleI_diverted; +volatile int sampleV; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define PERSISTENCE_FOR_POLARITY_CHANGE 1 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "RawSamplesTool_2chan.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + +// for this go-faster sketch, the phaseCal logic has been removed. If required, it can be +// found in most of the standard Mk2_bothDisplay_n versions + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define UPDATE_PERIOD_FOR_DISPLAYED_DATA 50 // mains cycles +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +// The two versions of the hardware require different logic. +#ifdef PIN_SAVING_HARDWARE + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. In this version, +// the decimal point has to be treated differently than the other seven segments, so +// a convenient means of accessing this column is provided. +// +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + +#else // PIN_SAVING_HARDWARE + +#define ON HIGH +#define OFF LOW + +const byte noOfSegmentsPerDigit = 8; // includes one for the decimal point +enum digitEnableStates {DIGIT_ENABLED, DIGIT_DISABLED}; + +byte digitSelectorPin[noOfDigitLocations] = {16,10,13,11}; +byte segmentDrivePin[noOfSegmentsPerDigit] = {2,5,12,6,7,9,8,14}; + +// The final column of segMap[] is for the decimal point status. In this version, +// the decimal point is treated just like all the other segments, so there is +// no need to access this column specifically. +// +byte segMap[noOfPossibleCharacters][noOfSegmentsPerDigit] = { + ON , ON , ON , ON , ON , ON , OFF, OFF, // '0' <- element 0 + OFF, ON , ON , OFF, OFF, OFF, OFF, OFF, // '1' <- element 1 + ON , ON , OFF, ON , ON , OFF, ON , OFF, // '2' <- element 2 + ON , ON , ON , ON , OFF, OFF, ON , OFF, // '3' <- element 3 + OFF, ON , ON , OFF, OFF, ON , ON , OFF, // '4' <- element 4 + ON , OFF, ON , ON , OFF, ON , ON , OFF, // '5' <- element 5 + ON , OFF, ON , ON , ON , ON , ON , OFF, // '6' <- element 6 + ON , ON , ON , OFF, OFF, OFF, OFF, OFF, // '7' <- element 7 + ON , ON , ON , ON , ON , ON , ON , OFF, // '8' <- element 8 + ON , ON , ON , ON , OFF, ON , ON , OFF, // '9' <- element 9 + ON , ON , ON , ON , ON , ON , OFF, ON , // '0.' <- element 10 + OFF, ON , ON , OFF, OFF, OFF, OFF, ON , // '1.' <- element 11 + ON , ON , OFF, ON , ON , OFF, ON , ON , // '2.' <- element 12 + ON , ON , ON , ON , OFF, OFF, ON , ON , // '3.' <- element 13 + OFF, ON , ON , OFF, OFF, ON , ON , ON , // '4.' <- element 14 + ON , OFF, ON , ON , OFF, ON , ON , ON , // '5.' <- element 15 + ON , OFF, ON , ON , ON , ON , ON , ON , // '6.' <- element 16 + ON , ON , ON , OFF, OFF, OFF, OFF, ON , // '7.' <- element 17 + ON , ON , ON , ON , ON , ON , ON , ON , // '8.' <- element 18 + ON , ON , ON , ON , OFF, ON , ON , ON , // '9.' <- element 19 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, // ' ' <- element 20 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, ON // '.' <- element 11 +}; +#endif // PIN_SAVING_HARDWARE + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // energy divertion detection +long requiredExportPerMainsCycle_inIEU; + + +void setup() +{ + pinMode(outOfRangeIndication, OUTPUT); + digitalWrite (outOfRangeIndication, LED_OFF); + + pinMode(outputForTrigger, OUTPUT); + digitalWrite (outputForTrigger, LOAD_OFF); + + delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_fasterControl_1a.ino"); + Serial.println(); + +#ifdef PIN_SAVING_HARDWARE + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } +#else + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + pinMode(segmentDrivePin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + pinMode(digitSelectorPin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + digitalWrite(digitSelectorPin[i], DIGIT_DISABLED); } + + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + digitalWrite(segmentDrivePin[i], OFF); } +#endif + + + + // When using integer maths, calibration values that have supplied in floating point + // form need to be rescaled. + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + energyInBucket_long = 0; + + singleEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + + Serial.println ("----"); +} + +// An Interrupt Service Routine is now defined in which the ADC is instructed to +// measure each analogue input in sequence. A "data ready" flag is set after each +// voltage conversion has been completed. +// For each set of samples, the two samples for current are taken before the one +// for voltage. This is appropriate because each waveform current is generally slightly +// advanced relative to the waveform for voltage. The data ready flag is cleared +// within loop(). +// This Interrupt Service Routine is for use when the ADC is fixed timer mode. It is +// executed whenever the ADC timer expires. In this mode, the next ADC conversion is +// initiated from within this ISR. +// +void timerIsr(void) +{ + static unsigned char sample_index = 0; + static int sampleI_grid_raw; + static int sampleI_diverted_raw; + + + switch(sample_index) + { + case 0: + sampleV = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current + ADCSRA |= (1< 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + if (beyondStartUpPhase) + { + // a simple routine for checking the performance of this new ISR structure + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / sampleSetsDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step would normally be to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient to just + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + bool endOfRangeEncountered = false; + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; + endOfRangeEncountered = true;} + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; + endOfRangeEncountered = true;} + + if (endOfRangeEncountered) { + digitalWrite (outOfRangeIndication , LED_ON); } + else { + digitalWrite (outOfRangeIndication , LED_OFF); } + + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. + + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) + { + realEnergy_diverted = 0; + } + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + if(timerForDisplayUpdate > UPDATE_PERIOD_FOR_DISPLAYED_DATA) + { // the 4-digit display needs to be refreshed every few mS. For convenience, + // this action is performed every N times around this processing loop. + timerForDisplayUpdate = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); + } + else + { + timerForDisplayUpdate++; + } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringNegativeHalfOfMainsCycle = 0; + + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringThisMainsCycle = 0; // not yet dealt with for this cycle + sampleCount_forContinuityChecker = 1; // opportunity has been missed for this cycle + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + // (in this go-faster code, the action from here has moved to the negative half of the cycle) + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12); + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + // The average power that has been measured during the first half of this mains cycle can now be used + // to predict the energy state at the end of this mains cycle. That prediction will be used to alter + // the state of the load as necessary. The arming signal for the triac can't be set yet - that must + // wait until the voltage has advanced further beyond the -ve going zero-crossing point. + // + long averagePower = sumP_grid / sampleSetsDuringThisMainsCycle;// for 1st half of this mains cycle only + // + // To avoid repetitive and unnecessary calculations, the increase in energy during each mains cycle is + // deemed to be numerically equal to the average power. The predicted value for the energy state at the + // end of this mains cycle will therefore be the known energy state at its start plus the average power + // as measured. Although the average power has been determined over only half a mains cycle, the correct + // number of contributing sample sets has been used so the result can be expected to be a true measurement + // of average power, not half of it. + // However, it can be shown that the average power during the first half of any mains cycle after the + // load has changed state will alway be under-recorded so its value should now be increased by 30%. This + // arbitrary looking adjustment gives good test results with differening amounts of surplus power and it + // only affects the predicted value of the energy state at the end of the current mains cycle; it does + // not affect the value in the main energy bucket. This complication is a fundamental limitation + // of the floating CTs that we use. + // + if (loadHasJustChangedState) + { + averagePower = averagePower * 1.3; + } + energyInBucket_prediction = energyInBucket_long + averagePower; // at end of this mains cycle + + + } // end of processing that is specific to the first Vsample in each -ve half cycle + + // check to see whether the trigger device can now be reliably armed + if (sampleSetsDuringNegativeHalfOfMainsCycle == 3) + { + if (beyondStartUpPhase) + { + enum loadStates prevStateOfLoad = nextStateOfLoad; + if (energyInBucket_prediction < singleEnergyThreshold_long) { + nextStateOfLoad = LOAD_OFF; } + else + nextStateOfLoad = LOAD_ON; + + if (nextStateOfLoad != prevStateOfLoad) { + loadHasJustChangedState = true; } + else { + loadHasJustChangedState = false; } + + // set the Arduino's output pin accordingly, and clear the flag + digitalWrite(outputForTrigger, nextStateOfLoad); + + // update the Energy Diversion Detector + if (nextStateOfLoad == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + } + } + + sampleSetsDuringNegativeHalfOfMainsCycle++; + } // end of processing that is specific to samples where the voltage is negative + + // processing for EVERY set of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + sampleSetsDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count > PERSISTENCE_FOR_POLARITY_CHANGE) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + +// Serial.println(divertedEnergyTotal_Wh); + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +} + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + // The two versions of the hardware require different logic. + +#ifdef PIN_SAVING_HARDWARE + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } + +#else // PIN_SAVING_HARDWARE + + // This version is more straightforward because the digit-enable lines can be + // used to mask out all of the transitory states, including the Decimal Point. + // The sequence is: + // + // 1. de-activate the digit-enable line that was previously active + // 2. determine the next location which is to be active + // 3. determine the relevant character for the new active location + // 4. set up the segment drivers for the character to be displayed (includes the DP) + // 5. activate the digit-enable line for the new active location + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + displayTime_count = 0; + + // 1. de-activate the location which is currently being displayed + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_DISABLED); + + // 2. determine the next digit location which is to be displayed + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 3. determine the relevant character for the new active location + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 4. set up the segment drivers for the character to be displayed (includes the DP) + for (byte segment = 0; segment < noOfSegmentsPerDigit; segment++) { + byte segmentState = segMap[digitVal][segment]; + digitalWrite( segmentDrivePin[segment], segmentState); } + + // 5. activate the digit-enable line for the new active location + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_ENABLED); + } +#endif // PIN_SAVING_HARDWARE + +} // end of refreshDisplay() + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_2.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_2.ino new file mode 100644 index 0000000..e0bff9c --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_2.ino @@ -0,0 +1,1005 @@ +/* Mk2_fasterControl_2.ino + * + * (initially released as Mk2_bothDisplays_1 in March 2014) + * This sketch is for diverting suplus PV power to a dump load using a triac or + * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on + * the OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * In this latest version, the pin-allocations have been changed to suit my + * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is + * fed from one of the secondary coils of the transformer. Current is measured + * via Current Transformers at the CT1 and CT1 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. This can be driven in two + * different ways, one with an extra pair of logic chips, and one without. The + * appropriate version of the sketch must be selected by including or commenting + * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code. + * + * September 2014: renamed as Mk2_bothDisplays_2, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - removal of unhelpful comments in the IO pin section; + * - tidier initialisation of display logic in setup(); + * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility); + * + * December 2014: renamed as Mk2_bothDisplays_3, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed) + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances + * + * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed. + * + * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to + * remove a timing uncertainty. + * + * January 2016: updated to Mk2_bothDisplays_3c: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * February 2016: updated to Mk2_bothDisplays_4, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * November 2019: updated to Mk2_fasterControl_1 with these changes: + * - Half way through each mains cycle, a prediction is made of the likely energy level at the + * end of the cycle. That predicted value allows the triac to be switched at the +ve going + * zero-crossing point rather than waiting for a further 10 ms. These changes allow for + * faster switching of the load. + * - The range of the energy bucket has been reduced to one tenth of its former value. This + * allows the unit's operation to commence more rapidly whenever surplus power is available. + * - controlMode is no longer selectable, the unit's operation being effectively hard-coded + * as "Normal" rather than Anti-flicker. + * - Port D3 now supports an indicator which shows when the level in the energy bucket + * reaches either end of its range. While the unit is actively diverting surplus power, + * it is vital that the level in the reduced capacity energy bucket remains within its + * permitted range, hence the addition of this indicator. + * + * February 2020: updated to Mk2_fasterControl_1a with these changes: + * - removal of some redundant code in the logic for determining the next load state. + * + * March 2021: updated to Mk2_fasterControl_2 with these changes: + * - extra filtering added to offset the HPF effect of CT1. This allows the energy state in + * 10 ms time to be predicted with more confidence. Specifically, it is no longer necessary + * to include a 30% boost factor after each change of load state. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define WORKING_RANGE_IN_JOULES 360 // 0.1 Wh, reduced for faster start-up +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +// The two versions of the hardware require different logic. The following line should +// be included if the additional logic chips are present, or excluded if they are +// absent (in which case some wire links need to be fitted) +// +#define PIN_SAVING_HARDWARE + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum ledStates {LED_OFF, LED_ON}; // for use at port D3 which is active-high +enum loadStates {LOAD_ON, LOAD_OFF}; // for use at port D4 which is active-low + +// For this go-faster version, the unit's operation will effectively always be "Normal"; +// there is no "Anti-flicker" option. The controlMode variable has been removed. + +// allocation of digital pins which are not dependent on the display type that is in use +// ************************************************************************************* +const byte outOfRangeIndication = 3; // <-- this output port is active-high +const byte outputForTrigger = 4; // <-- this output port is active-low + +// allocation of analogue pins which are not dependent on the display type that is in use +// ************************************************************************************** +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + +const byte delayBeforeSerialStarts = 1; // in seconds, to allow Serial window to be opened +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long singleEnergyThreshold_long; // for go-faster use + +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; +long mainsCyclesPerHour; + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +volatile int sampleI_grid; +volatile int sampleI_diverted; +volatile int sampleV; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define PERSISTENCE_FOR_POLARITY_CHANGE 1 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "RawSamplesTool_2chan.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + +// for this go-faster sketch, the phaseCal logic has been removed. If required, it can be +// found in most of the standard Mk2_bothDisplay_n versions + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define UPDATE_PERIOD_FOR_DISPLAYED_DATA 50 // mains cycles +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +// The two versions of the hardware require different logic. +#ifdef PIN_SAVING_HARDWARE + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. In this version, +// the decimal point has to be treated differently than the other seven segments, so +// a convenient means of accessing this column is provided. +// +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + +#else // PIN_SAVING_HARDWARE + +#define ON HIGH +#define OFF LOW + +const byte noOfSegmentsPerDigit = 8; // includes one for the decimal point +enum digitEnableStates {DIGIT_ENABLED, DIGIT_DISABLED}; + +byte digitSelectorPin[noOfDigitLocations] = {16,10,13,11}; +byte segmentDrivePin[noOfSegmentsPerDigit] = {2,5,12,6,7,9,8,14}; + +// The final column of segMap[] is for the decimal point status. In this version, +// the decimal point is treated just like all the other segments, so there is +// no need to access this column specifically. +// +byte segMap[noOfPossibleCharacters][noOfSegmentsPerDigit] = { + ON , ON , ON , ON , ON , ON , OFF, OFF, // '0' <- element 0 + OFF, ON , ON , OFF, OFF, OFF, OFF, OFF, // '1' <- element 1 + ON , ON , OFF, ON , ON , OFF, ON , OFF, // '2' <- element 2 + ON , ON , ON , ON , OFF, OFF, ON , OFF, // '3' <- element 3 + OFF, ON , ON , OFF, OFF, ON , ON , OFF, // '4' <- element 4 + ON , OFF, ON , ON , OFF, ON , ON , OFF, // '5' <- element 5 + ON , OFF, ON , ON , ON , ON , ON , OFF, // '6' <- element 6 + ON , ON , ON , OFF, OFF, OFF, OFF, OFF, // '7' <- element 7 + ON , ON , ON , ON , ON , ON , ON , OFF, // '8' <- element 8 + ON , ON , ON , ON , OFF, ON , ON , OFF, // '9' <- element 9 + ON , ON , ON , ON , ON , ON , OFF, ON , // '0.' <- element 10 + OFF, ON , ON , OFF, OFF, OFF, OFF, ON , // '1.' <- element 11 + ON , ON , OFF, ON , ON , OFF, ON , ON , // '2.' <- element 12 + ON , ON , ON , ON , OFF, OFF, ON , ON , // '3.' <- element 13 + OFF, ON , ON , OFF, OFF, ON , ON , ON , // '4.' <- element 14 + ON , OFF, ON , ON , OFF, ON , ON , ON , // '5.' <- element 15 + ON , OFF, ON , ON , ON , ON , ON , ON , // '6.' <- element 16 + ON , ON , ON , OFF, OFF, OFF, OFF, ON , // '7.' <- element 17 + ON , ON , ON , ON , ON , ON , ON , ON , // '8.' <- element 18 + ON , ON , ON , ON , OFF, ON , ON , ON , // '9.' <- element 19 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, // ' ' <- element 20 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, ON // '.' <- element 11 +}; +#endif // PIN_SAVING_HARDWARE + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // energy divertion detection +long requiredExportPerMainsCycle_inIEU; + + +void setup() +{ + pinMode(outOfRangeIndication, OUTPUT); + digitalWrite (outOfRangeIndication, LED_OFF); + + pinMode(outputForTrigger, OUTPUT); + digitalWrite (outputForTrigger, LOAD_OFF); + + delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_fasterControl_2.ino"); + Serial.println(); + +#ifdef PIN_SAVING_HARDWARE + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } +#else + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + pinMode(segmentDrivePin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + pinMode(digitSelectorPin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + digitalWrite(digitSelectorPin[i], DIGIT_DISABLED); } + + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + digitalWrite(segmentDrivePin[i], OFF); } +#endif + + + + // When using integer maths, calibration values that have supplied in floating point + // form need to be rescaled. + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + energyInBucket_long = 0; + + singleEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + + Serial.println ("----"); +} + +// An Interrupt Service Routine is now defined in which the ADC is instructed to +// measure each analogue input in sequence. A "data ready" flag is set after each +// voltage conversion has been completed. +// For each set of samples, the two samples for current are taken before the one +// for voltage. This is appropriate because each waveform current is generally slightly +// advanced relative to the waveform for voltage. The data ready flag is cleared +// within loop(). +// This Interrupt Service Routine is for use when the ADC is fixed timer mode. It is +// executed whenever the ADC timer expires. In this mode, the next ADC conversion is +// initiated from within this ISR. +// +void timerIsr(void) +{ + static unsigned char sample_index = 0; + static int sampleI_grid_raw; + static int sampleI_diverted_raw; + + + switch(sample_index) + { + case 0: + sampleV = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current + ADCSRA |= (1< 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + if (beyondStartUpPhase) + { + // a simple routine for checking the performance of this new ISR structure + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / sampleSetsDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step would normally be to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient to just + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + bool endOfRangeEncountered = false; + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; + endOfRangeEncountered = true;} + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; + endOfRangeEncountered = true;} + + if (endOfRangeEncountered) { + digitalWrite (outOfRangeIndication , LED_ON); } + else { + digitalWrite (outOfRangeIndication , LED_OFF); } + + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. + + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) + { + realEnergy_diverted = 0; + } + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + if(timerForDisplayUpdate > UPDATE_PERIOD_FOR_DISPLAYED_DATA) + { // the 4-digit display needs to be refreshed every few mS. For convenience, + // this action is performed every N times around this processing loop. + timerForDisplayUpdate = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); + } + else + { + timerForDisplayUpdate++; + } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringNegativeHalfOfMainsCycle = 0; + + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringThisMainsCycle = 0; // not yet dealt with for this cycle + sampleCount_forContinuityChecker = 1; // opportunity has been missed for this cycle + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + // (in this go-faster code, the action from here has moved to the negative half of the cycle) + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12); + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + // The average power that has been measured during the first half of this mains cycle can now be used + // to predict the energy state at the end of this mains cycle. That prediction will be used to alter + // the state of the load as necessary. The arming signal for the triac can't be set yet - that must + // wait until the voltage has advanced further beyond the -ve going zero-crossing point. + // + long averagePower = sumP_grid / sampleSetsDuringThisMainsCycle;// for 1st half of this mains cycle only + // + // To avoid repetitive and unnecessary calculations, the increase in energy during each mains cycle is + // deemed to be numerically equal to the average power. The predicted value for the energy state at the + // end of this mains cycle will therefore be the known energy state at its start plus the average power + // as measured. Although the average power has been determined over only half a mains cycle, the correct + // number of contributing sample sets has been used so the result can be expected to be a true measurement + // of average power, not half of it. + // + energyInBucket_prediction = energyInBucket_long + averagePower; // at end of this mains cycle + + + } // end of processing that is specific to the first Vsample in each -ve half cycle + + // check to see whether the trigger device can now be reliably armed + if (sampleSetsDuringNegativeHalfOfMainsCycle == 3) + { + if (beyondStartUpPhase) + { + enum loadStates prevStateOfLoad = nextStateOfLoad; + if (energyInBucket_prediction < singleEnergyThreshold_long) { + nextStateOfLoad = LOAD_OFF; } + else + nextStateOfLoad = LOAD_ON; + + // set the Arduino's output pin accordingly, and clear the flag + digitalWrite(outputForTrigger, nextStateOfLoad); + + // update the Energy Diversion Detector + if (nextStateOfLoad == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + } + } + + sampleSetsDuringNegativeHalfOfMainsCycle++; + } // end of processing that is specific to samples where the voltage is negative + + // processing for EVERY set of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + // + // extra filtering to offset the HPF effect of CT1 + long last_lpf_long = lpf_long; + lpf_long = last_lpf_long + alpha *(sampleIminusDC_grid - last_lpf_long); + sampleIminusDC_grid += (lpf_gain * lpf_long); + + // + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + sampleSetsDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count > PERSISTENCE_FOR_POLARITY_CHANGE) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + +// Serial.println(divertedEnergyTotal_Wh); + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +} + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + // The two versions of the hardware require different logic. + +#ifdef PIN_SAVING_HARDWARE + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } + +#else // PIN_SAVING_HARDWARE + + // This version is more straightforward because the digit-enable lines can be + // used to mask out all of the transitory states, including the Decimal Point. + // The sequence is: + // + // 1. de-activate the digit-enable line that was previously active + // 2. determine the next location which is to be active + // 3. determine the relevant character for the new active location + // 4. set up the segment drivers for the character to be displayed (includes the DP) + // 5. activate the digit-enable line for the new active location + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + displayTime_count = 0; + + // 1. de-activate the location which is currently being displayed + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_DISABLED); + + // 2. determine the next digit location which is to be displayed + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 3. determine the relevant character for the new active location + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 4. set up the segment drivers for the character to be displayed (includes the DP) + for (byte segment = 0; segment < noOfSegmentsPerDigit; segment++) { + byte segmentState = segMap[digitVal][segment]; + digitalWrite( segmentDrivePin[segment], segmentState); } + + // 5. activate the digit-enable line for the new active location + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_ENABLED); + } +#endif // PIN_SAVING_HARDWARE + +} // end of refreshDisplay() + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_3.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_3.ino new file mode 100644 index 0000000..99a356f --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_3.ino @@ -0,0 +1,1009 @@ +/* Mk2_fasterControl_3.ino + * + * (initially released as Mk2_bothDisplays_1 in March 2014) + * This sketch is for diverting suplus PV power to a dump load using a triac or + * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on + * the OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * In this latest version, the pin-allocations have been changed to suit my + * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is + * fed from one of the secondary coils of the transformer. Current is measured + * via Current Transformers at the CT1 and CT1 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. This can be driven in two + * different ways, one with an extra pair of logic chips, and one without. The + * appropriate version of the sketch must be selected by including or commenting + * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code. + * + * September 2014: renamed as Mk2_bothDisplays_2, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - removal of unhelpful comments in the IO pin section; + * - tidier initialisation of display logic in setup(); + * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility); + * + * December 2014: renamed as Mk2_bothDisplays_3, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed) + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances + * + * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed. + * + * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to + * remove a timing uncertainty. + * + * January 2016: updated to Mk2_bothDisplays_3c: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * February 2016: updated to Mk2_bothDisplays_4, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * November 2019: updated to Mk2_fasterControl_1 with these changes: + * - Half way through each mains cycle, a prediction is made of the likely energy level at the + * end of the cycle. That predicted value allows the triac to be switched at the +ve going + * zero-crossing point rather than waiting for a further 10 ms. These changes allow for + * faster switching of the load. + * - The range of the energy bucket has been reduced to one tenth of its former value. This + * allows the unit's operation to commence more rapidly whenever surplus power is available. + * - controlMode is no longer selectable, the unit's operation being effectively hard-coded + * as "Normal" rather than Anti-flicker. + * - Port D3 now supports an indicator which shows when the level in the energy bucket + * reaches either end of its range. While the unit is actively diverting surplus power, + * it is vital that the level in the reduced capacity energy bucket remains within its + * permitted range, hence the addition of this indicator. + * + * February 2020: updated to Mk2_fasterControl_1a with these changes: + * - removal of some redundant code in the logic for determining the next load state. + * + * March 2021: updated to Mk2_fasterControl_2 with these changes: + * - extra filtering added to offset the HPF effect of CT1. This allows the energy state in + * 10 ms time to be predicted with more confidence. Specifically, it is no longer necessary + * to include a 30% boost factor after each change of load state. + * + * June 2021: updated to Mk2_fasterControl_3 with these changes: + * - to reflect the performance of recently manufactured YHDC SCT_013_000 CTs, + * the value of the parameter lpf_gain has been reduced from 12 to 8. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define WORKING_RANGE_IN_JOULES 360 // 0.1 Wh, reduced for faster start-up +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +// The two versions of the hardware require different logic. The following line should +// be included if the additional logic chips are present, or excluded if they are +// absent (in which case some wire links need to be fitted) +// +//#define PIN_SAVING_HARDWARE + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum ledStates {LED_OFF, LED_ON}; // for use at port D3 which is active-high +enum loadStates {LOAD_ON, LOAD_OFF}; // for use at port D4 which is active-low + +// For this go-faster version, the unit's operation will effectively always be "Normal"; +// there is no "Anti-flicker" option. The controlMode variable has been removed. + +// allocation of digital pins which are not dependent on the display type that is in use +// ************************************************************************************* +const byte outOfRangeIndication = 3; // <-- this output port is active-high +const byte outputForTrigger = 4; // <-- this output port is active-low + +// allocation of analogue pins which are not dependent on the display type that is in use +// ************************************************************************************** +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + +const byte delayBeforeSerialStarts = 1; // in seconds, to allow Serial window to be opened +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long singleEnergyThreshold_long; // for go-faster use + +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; +long mainsCyclesPerHour; + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +volatile int sampleI_grid; +volatile int sampleI_diverted; +volatile int sampleV; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define PERSISTENCE_FOR_POLARITY_CHANGE 1 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "RawSamplesTool_2chan.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + +// for this go-faster sketch, the phaseCal logic has been removed. If required, it can be +// found in most of the standard Mk2_bothDisplay_n versions + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define UPDATE_PERIOD_FOR_DISPLAYED_DATA 50 // mains cycles +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +// The two versions of the hardware require different logic. +#ifdef PIN_SAVING_HARDWARE + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. In this version, +// the decimal point has to be treated differently than the other seven segments, so +// a convenient means of accessing this column is provided. +// +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + +#else // PIN_SAVING_HARDWARE + +#define ON HIGH +#define OFF LOW + +const byte noOfSegmentsPerDigit = 8; // includes one for the decimal point +enum digitEnableStates {DIGIT_ENABLED, DIGIT_DISABLED}; + +byte digitSelectorPin[noOfDigitLocations] = {16,10,13,11}; +byte segmentDrivePin[noOfSegmentsPerDigit] = {2,5,12,6,7,9,8,14}; + +// The final column of segMap[] is for the decimal point status. In this version, +// the decimal point is treated just like all the other segments, so there is +// no need to access this column specifically. +// +byte segMap[noOfPossibleCharacters][noOfSegmentsPerDigit] = { + ON , ON , ON , ON , ON , ON , OFF, OFF, // '0' <- element 0 + OFF, ON , ON , OFF, OFF, OFF, OFF, OFF, // '1' <- element 1 + ON , ON , OFF, ON , ON , OFF, ON , OFF, // '2' <- element 2 + ON , ON , ON , ON , OFF, OFF, ON , OFF, // '3' <- element 3 + OFF, ON , ON , OFF, OFF, ON , ON , OFF, // '4' <- element 4 + ON , OFF, ON , ON , OFF, ON , ON , OFF, // '5' <- element 5 + ON , OFF, ON , ON , ON , ON , ON , OFF, // '6' <- element 6 + ON , ON , ON , OFF, OFF, OFF, OFF, OFF, // '7' <- element 7 + ON , ON , ON , ON , ON , ON , ON , OFF, // '8' <- element 8 + ON , ON , ON , ON , OFF, ON , ON , OFF, // '9' <- element 9 + ON , ON , ON , ON , ON , ON , OFF, ON , // '0.' <- element 10 + OFF, ON , ON , OFF, OFF, OFF, OFF, ON , // '1.' <- element 11 + ON , ON , OFF, ON , ON , OFF, ON , ON , // '2.' <- element 12 + ON , ON , ON , ON , OFF, OFF, ON , ON , // '3.' <- element 13 + OFF, ON , ON , OFF, OFF, ON , ON , ON , // '4.' <- element 14 + ON , OFF, ON , ON , OFF, ON , ON , ON , // '5.' <- element 15 + ON , OFF, ON , ON , ON , ON , ON , ON , // '6.' <- element 16 + ON , ON , ON , OFF, OFF, OFF, OFF, ON , // '7.' <- element 17 + ON , ON , ON , ON , ON , ON , ON , ON , // '8.' <- element 18 + ON , ON , ON , ON , OFF, ON , ON , ON , // '9.' <- element 19 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, // ' ' <- element 20 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, ON // '.' <- element 11 +}; +#endif // PIN_SAVING_HARDWARE + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // energy divertion detection +long requiredExportPerMainsCycle_inIEU; + + +void setup() +{ + pinMode(outOfRangeIndication, OUTPUT); + digitalWrite (outOfRangeIndication, LED_OFF); + + pinMode(outputForTrigger, OUTPUT); + digitalWrite (outputForTrigger, LOAD_OFF); + + delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_fasterControl_3.ino"); + Serial.println(); + +#ifdef PIN_SAVING_HARDWARE + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } +#else + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + pinMode(segmentDrivePin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + pinMode(digitSelectorPin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + digitalWrite(digitSelectorPin[i], DIGIT_DISABLED); } + + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + digitalWrite(segmentDrivePin[i], OFF); } +#endif + + + + // When using integer maths, calibration values that have supplied in floating point + // form need to be rescaled. + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + energyInBucket_long = 0; + + singleEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + + Serial.println ("----"); +} + +// An Interrupt Service Routine is now defined in which the ADC is instructed to +// measure each analogue input in sequence. A "data ready" flag is set after each +// voltage conversion has been completed. +// For each set of samples, the two samples for current are taken before the one +// for voltage. This is appropriate because each waveform current is generally slightly +// advanced relative to the waveform for voltage. The data ready flag is cleared +// within loop(). +// This Interrupt Service Routine is for use when the ADC is fixed timer mode. It is +// executed whenever the ADC timer expires. In this mode, the next ADC conversion is +// initiated from within this ISR. +// +void timerIsr(void) +{ + static unsigned char sample_index = 0; + static int sampleI_grid_raw; + static int sampleI_diverted_raw; + + + switch(sample_index) + { + case 0: + sampleV = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current + ADCSRA |= (1< 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + if (beyondStartUpPhase) + { + // a simple routine for checking the performance of this new ISR structure + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / sampleSetsDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step would normally be to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient to just + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + bool endOfRangeEncountered = false; + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; + endOfRangeEncountered = true;} + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; + endOfRangeEncountered = true;} + + if (endOfRangeEncountered) { + digitalWrite (outOfRangeIndication , LED_ON); } + else { + digitalWrite (outOfRangeIndication , LED_OFF); } + + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. + + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) + { + realEnergy_diverted = 0; + } + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + if(timerForDisplayUpdate > UPDATE_PERIOD_FOR_DISPLAYED_DATA) + { // the 4-digit display needs to be refreshed every few mS. For convenience, + // this action is performed every N times around this processing loop. + timerForDisplayUpdate = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); + } + else + { + timerForDisplayUpdate++; + } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringNegativeHalfOfMainsCycle = 0; + + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringThisMainsCycle = 0; // not yet dealt with for this cycle + sampleCount_forContinuityChecker = 1; // opportunity has been missed for this cycle + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + // (in this go-faster code, the action from here has moved to the negative half of the cycle) + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12); + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + // The average power that has been measured during the first half of this mains cycle can now be used + // to predict the energy state at the end of this mains cycle. That prediction will be used to alter + // the state of the load as necessary. The arming signal for the triac can't be set yet - that must + // wait until the voltage has advanced further beyond the -ve going zero-crossing point. + // + long averagePower = sumP_grid / sampleSetsDuringThisMainsCycle;// for 1st half of this mains cycle only + // + // To avoid repetitive and unnecessary calculations, the increase in energy during each mains cycle is + // deemed to be numerically equal to the average power. The predicted value for the energy state at the + // end of this mains cycle will therefore be the known energy state at its start plus the average power + // as measured. Although the average power has been determined over only half a mains cycle, the correct + // number of contributing sample sets has been used so the result can be expected to be a true measurement + // of average power, not half of it. + // + energyInBucket_prediction = energyInBucket_long + averagePower; // at end of this mains cycle + + + } // end of processing that is specific to the first Vsample in each -ve half cycle + + // check to see whether the trigger device can now be reliably armed + if (sampleSetsDuringNegativeHalfOfMainsCycle == 3) + { + if (beyondStartUpPhase) + { + enum loadStates prevStateOfLoad = nextStateOfLoad; + if (energyInBucket_prediction < singleEnergyThreshold_long) { + nextStateOfLoad = LOAD_OFF; } + else + nextStateOfLoad = LOAD_ON; + + // set the Arduino's output pin accordingly, and clear the flag + digitalWrite(outputForTrigger, nextStateOfLoad); + + // update the Energy Diversion Detector + if (nextStateOfLoad == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + } + } + + sampleSetsDuringNegativeHalfOfMainsCycle++; + } // end of processing that is specific to samples where the voltage is negative + + // processing for EVERY set of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + // + // extra filtering to offset the HPF effect of CT1 + long last_lpf_long = lpf_long; + lpf_long = last_lpf_long + alpha *(sampleIminusDC_grid - last_lpf_long); + sampleIminusDC_grid += (lpf_gain * lpf_long); + + // + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + sampleSetsDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count > PERSISTENCE_FOR_POLARITY_CHANGE) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + +// Serial.println(divertedEnergyTotal_Wh); + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +} + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + // The two versions of the hardware require different logic. + +#ifdef PIN_SAVING_HARDWARE + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } + +#else // PIN_SAVING_HARDWARE + + // This version is more straightforward because the digit-enable lines can be + // used to mask out all of the transitory states, including the Decimal Point. + // The sequence is: + // + // 1. de-activate the digit-enable line that was previously active + // 2. determine the next location which is to be active + // 3. determine the relevant character for the new active location + // 4. set up the segment drivers for the character to be displayed (includes the DP) + // 5. activate the digit-enable line for the new active location + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + displayTime_count = 0; + + // 1. de-activate the location which is currently being displayed + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_DISABLED); + + // 2. determine the next digit location which is to be displayed + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 3. determine the relevant character for the new active location + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 4. set up the segment drivers for the character to be displayed (includes the DP) + for (byte segment = 0; segment < noOfSegmentsPerDigit; segment++) { + byte segmentState = segMap[digitVal][segment]; + digitalWrite( segmentDrivePin[segment], segmentState); } + + // 5. activate the digit-enable line for the new active location + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_ENABLED); + } +#endif // PIN_SAVING_HARDWARE + +} // end of refreshDisplay() + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_RFdatalog_1.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_RFdatalog_1.ino new file mode 100644 index 0000000..3dd87d1 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_RFdatalog_1.ino @@ -0,0 +1,1203 @@ +/* Mk2_fasterControl_RFdatalog_1.ino + * + * (initially released as Mk2_bothDisplays_1 in March 2014) + * This sketch is for diverting suplus PV power to a dump load using a triac or + * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on + * the OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * In this latest version, the pin-allocations have been changed to suit my + * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is + * fed from one of the secondary coils of the transformer. Current is measured + * via Current Transformers at the CT1 and CT1 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. With this sketch that supports datalogging + * via RF, the display can only be used with the pin-saving hardware: ICs 3&4. + * + * September 2014: renamed as Mk2_bothDisplays_2, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - removal of unhelpful comments in the IO pin section; + * - tidier initialisation of display logic in setup(); + * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility); + * + * December 2014: renamed as Mk2_bothDisplays_3, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed) + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances + * + * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed. + * + * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to + * remove a timing uncertainty. + * + * January 2016: updated to Mk2_bothDisplays_3c: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * February 2016: updated to Mk2_bothDisplays_4, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * November 2019: updated to Mk2_fasterControl_1 with these changes: + * - Half way through each mains cycle, a prediction is made of the likely energy level at the + * end of the cycle. That predicted value allows the triac to be switched at the +ve going + * zero-crossing point rather than waiting for a further 10 ms. These changes allow for + * faster switching of the load. + * - The range of the energy bucket has been reduced to one tenth of its former value. This + * allows the unit's operation to commence more rapidly whenever surplus power is available. + * - controlMode is no longer selectable, the unit's operation being effectively hard-coded + * as "Normal" rather than Anti-flicker. + * - Port D3 now supports an indicator which shows when the level in the energy bucket + * reaches either end of its range. While the unit is actively diverting surplus power, + * it is vital that the level in the reduced capacity energy bucket remains within its + * permitted range, hence the addition of this indicator. + * + * February 2020: updated to Mk2_fasterControl_twoLoads_1 with these changes: + * - the energy overflow indicator has been disabled to free up port D3 + * - port D3 now supports a second load + * + * February 2020: updated to Mk2_fasterControl_twoLoads_2 with these changes: + * - improved multi-load control logic to prevent the primary load from being disturbed by + * the lower priority one. This logic now mirrors that in the Mk2_multiLoad_wired_n line. + * + * March 2021: updated to Mk2_fasterControl_withRF_1 with these changes: + * - addition of datalogging by RF + * - removal of the option for standard display hardware (which is incompatible with RF) + * + * March 2021: updated to Mk2_fasterControl_2 with these changes: + * - extra filtering added to offset the HPF effect of CT1. This allows the energy state in + * 10 ms time to be predicted with more confidence. Specifically, it is no longer necessary + * to include a 30% boost factor after each change of load state. + * + * June 2021: updated to Mk2_fasterControl_withRF_3 with these changes: + * - to reflect the performance of recently manufactured YHDC SCT_013_000 CTs, + * the value of the parameter lpf_gain has been reduced from 12 to 8. + * + * July 2022: updated to Mk2_fasterControl_withRF_4, with this change: + * - the datalogging accumulators for grid power, diverted power and Vsquared have been rescaled + * to 1/16 of their previous values to avoid the risk of overflowing during a 10-second + * datalogging period. + * + * September 2022: updated to Mk2_fasterControl_RFdatalog_1, with this change: + * - reinstated the code for constraining the energy bucket's level to within its + * working range. This important section of code has unfortunately been missing + * in all versions of the fasterControl_withRF line. The new name should make the + * purpose of this sketch more obvious. + * + * Robin Emley + * + * www.Mk2PVrouter.co.uk + */ + +#include +#include + +#define RF_PRESENT // <- this line should be commented out if the RFM12B module is not present +// +#ifdef RF_PRESENT +#define RF69_COMPAT 0 // for the RFM12B +// #define RF69_COMPAT 1 // for the RF69 +#include +#endif + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define RF_SEND_PERIOD 10 // seconds +#define WORKING_RANGE_IN_JOULES 360 // 0.1 Wh, reduced for faster start-up +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +const byte noOfDumploads = 2; + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are logically active-low +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +// For this go-faster version, the unit's operation will effectively always be "Normal"; +// there is no "Anti-flicker" option. The controlMode variable has therefore been removed. + +/* -------------------------------------- + * RF configuration (for the RFM12B of RF69CW module) + * frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ + */ +#ifdef RF_PRESENT +#define freq RF12_433MHZ + +const int nodeID = 10; // RFM12B node ID +const int networkGroup = 210; // wireless network group - needs to be same for all nodes +const int UNO = 1; // for when the processor contains the UNO bootloader. +#endif + +typedef struct { + int powerAtSupplyPoint_Watts; // import = +ve, to match OEM convention + int divertedEnergyTotal_Wh; // always positive + int divertedPower_Watts; // always positive + int Vrms_times100; +} Tx_struct; +Tx_struct tx_data; + +// allocation of digital IO pins +// ***************************** +// const byte outOfRangeIndication = 3; // <-- this output port is active-high +const byte physicalLoad_1_pin = 3; // <-- the "mode" port is active-high +const byte physicalLoad_0_pin = 4; // <-- the "trigger" port is active-low + +// allocation of analogue IO pins +// ****************************** +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + +const byte delayBeforeSerialStarts = 1; // in seconds, to allow Serial window to be opened +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long nominalEnergyThreshold; +long workingEnergyThreshold_upper; +long workingEnergyThreshold_lower; + +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh_diverted; + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; + +#define POST_TRANSITION_MAX_COUNT 3 // <-- allows each transition to take effect + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +volatile int sampleI_grid; +volatile int sampleI_diverted; +volatile int sampleV; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define PERSISTENCE_FOR_POLARITY_CHANGE 1 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and voltageCal. +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "RawSamplesTool_2chan.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-Vcc and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 230V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 230V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt. +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.062; +const float powerCal_diverted = 0.062; + +// for this go-faster sketch, the phaseCal logic has been removed. If required, it can be +// found in most of the standard Mk2_bothDisplay_n versions + +// For datalogging purposes, voltageCal has been included. When running at +// 230 V AC, the range of ADC values will be similar to the actual range of volts, +// so the optimal value for this cal factor will be close to unity. +// +const float voltageCal = 1.0; + + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +// This sketch can only use the pin-saving hardware (ICs 3&4). +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. In this version, +// the decimal point has to be treated differently than the other seven segments, so +// a convenient means of accessing this column is provided. +// +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // energy divertion detection +long requiredExportPerMainsCycle_inIEU; + + +void setup() +{ +// pinMode(outOfRangeIndication, OUTPUT); +// digitalWrite (outOfRangeIndication, LED_OFF); + + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for the primary load + pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for an additional load + // + for(int i = 0; i< noOfDumploads; i++) // re-using the logic from my multiLoad code + { + logicalLoadState[i] = LOAD_OFF; + physicalLoadState[i] = LOAD_OFF; + } + // + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // the primary load is active low. + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // the additional load is active high. + + delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_fasterControl_RFdatalog_1.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, calibration values that have supplied in floating point + // form need to be rescaled. + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + energyInBucket_long = 0; + + nominalEnergyThreshold = capacityOfEnergyBucket_long * 0.5; + workingEnergyThreshold_upper = nominalEnergyThreshold; // initial value + workingEnergyThreshold_lower = nominalEnergyThreshold; // initial value + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + + IEU_per_Wh_diverted = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + + Serial.print ("RF capability "); + +#ifdef RF_PRESENT + Serial.print ("IS present, freq = "); + if (freq == RF12_433MHZ) { Serial.println ("433 MHz"); } + if (freq == RF12_868MHZ) { Serial.println ("868 MHz"); } + rf12_initialize(nodeID, freq, networkGroup); // initialize RF +#else + Serial.println ("is NOT present"); +#endif + + Serial.println ("----"); +} + +// An Interrupt Service Routine is now defined in which the ADC is instructed to +// measure each analogue input in sequence. A "data ready" flag is set after each +// voltage conversion has been completed. +// For each set of samples, the two samples for current are taken before the one +// for voltage. This is appropriate because each waveform current is generally slightly +// advanced relative to the waveform for voltage. The data ready flag is cleared +// within loop(). +// This Interrupt Service Routine is for use when the ADC is fixed timer mode. It is +// executed whenever the ADC timer expires. In this mode, the next ADC conversion is +// initiated from within this ISR. +// +void timerIsr(void) +{ + static unsigned char sample_index = 0; + static int sampleI_grid_raw; + static int sampleI_diverted_raw; + + + switch(sample_index) + { + case 0: + sampleV = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current + ADCSRA |= (1< 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + if (beyondStartUpPhase) + { + // a simple routine for checking the performance of the sampling structure + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / sampleSetsDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step would normally be to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient to just + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is generally set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. + + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) + { + realEnergy_diverted = 0; + } + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh_diverted) + { + divertedEnergyRecent_IEU -= IEU_per_Wh_diverted; + divertedEnergyTotal_Wh++; + } + } + + if(perSecondCounter > CYCLES_PER_SECOND) + { + perSecondCounter = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. It's checked every second. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + // routine data is to be transmitted every N seconds + RF_send_counter++; + if (RF_send_counter >= RF_SEND_PERIOD) + { + RF_send_counter = 0; + + // To provide sufficient range for a dataloging period of 10 seconds, the accumulators for grid power + // and diverted power are now scaled at 1/16 of their previous V_ADC * I_ADC values. Hence the * 16 factor + // that appears below. + // Similarly, the accumulator for Vsquared is now scaled at 1/16 of its previous V_ADC * V_ADC value. + // Hence the * 4 factor that appears below after the sqrt() operation. + // + long realPower_long = sumP_atSupplyPoint / samplesDuringDatalogPeriod; + tx_data.powerAtSupplyPoint_Watts = realPower_long * powerCal_grid * 16; + tx_data.powerAtSupplyPoint_Watts *= -1; // To match the OEM convention (so import is +ve) + realPower_long = sumP_forDivertedPower / samplesDuringDatalogPeriod; + tx_data.divertedPower_Watts = realPower_long * powerCal_diverted * 16; + tx_data.Vrms_times100 = + (int)(100 * voltageCal * sqrt(sum_Vsquared / samplesDuringDatalogPeriod) * 4); + tx_data.divertedEnergyTotal_Wh = divertedEnergyTotal_Wh; +#ifdef RF_PRESENT + send_rf_data(); +#endif +// +// Serial.println(tx_data.powerAtSupplyPoint_Watts); +// Serial.print(", "); +// Serial.println(tx_data.divertedPower_Watts); +// Serial.print(", "); + Serial.println(tx_data.Vrms_times100); +// + sumP_atSupplyPoint = 0; + sumP_forDivertedPower = 0; + sum_Vsquared = 0; + samplesDuringDatalogPeriod = 0; + } + + configureValueForDisplay(); + } + else + { + perSecondCounter++; + } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringNegativeHalfOfMainsCycle = 0; + + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringThisMainsCycle = 0; // not yet dealt with for this cycle + sampleCount_forContinuityChecker = 1; // opportunity has been missed for this cycle + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + if(sampleSetsDuringThisMainsCycle == 5) // to distribute the workload within each mains cycle + { + if (beyondStartUpPhase) + { + // Restrictions apply for the period immediately after a load has been switched. + // Here the recentTransition flag is checked and updated as necessary. + if (recentTransition) + { + postTransitionCount++; + if (postTransitionCount >= POST_TRANSITION_MAX_COUNT) + { + recentTransition = false; + } + } + + if (recentTransition) + { + // During the post-transition period, any change in the energy level is noted. + if (energyInBucket_long > workingEnergyThreshold_upper) + { + workingEnergyThreshold_lower = nominalEnergyThreshold; // reset the opposite threshold + workingEnergyThreshold_upper = energyInBucket_long; + + // the energy thresholds must remain within range + if (workingEnergyThreshold_upper > capacityOfEnergyBucket_long) + { + workingEnergyThreshold_upper = capacityOfEnergyBucket_long; + } + } + else + if (energyInBucket_long < workingEnergyThreshold_lower) + { + workingEnergyThreshold_upper = nominalEnergyThreshold; // reset the opposite threshold + workingEnergyThreshold_lower = energyInBucket_long; + + // the energy thresholds must remain within range + if (workingEnergyThreshold_lower < 0) + { + workingEnergyThreshold_lower = 0; + } + } + } + } + } + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12); + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + // The average power that has been measured during the first half of this mains cycle can now be used + // to predict the energy state at the end of this mains cycle. That prediction will be used to alter + // the state of the load as necessary. The arming signal for the triac can't be set yet - that must + // wait until the voltage has advanced further beyond the -ve going zero-crossing point. + // + long averagePower = sumP_grid / sampleSetsDuringThisMainsCycle;// for 1st half of this mains cycle only + // + // To avoid repetitive and unnecessary calculations, the increase in energy during each mains cycle is + // deemed to be numerically equal to the average power. The predicted value for the energy state at the + // end of this mains cycle will therefore be the known energy state at its start plus the average power + // as measured. Although the average power has been determined over only half a mains cycle, the correct + // number of contributing sample sets has been used so the result can be expected to be a true measurement + // of average power, not half of it. + energyInBucket_prediction = energyInBucket_long + averagePower; // at end of this mains cycle + + + } // end of processing that is specific to the first Vsample in each -ve half cycle + + if (sampleSetsDuringNegativeHalfOfMainsCycle == 5) + { + // the zero-crossing trigger device(s) can now be reliably armed + if (beyondStartUpPhase) + { + /* Determining whether any of the loads need to be changed is is a 3-stage process: + * - change the LOGICAL load states as necessary to maintain the energy level + * - update the PHYSICAL load states according to the logical -> physical mapping + * - update the driver lines for each of the loads. + */ + + if (energyInBucket_prediction > workingEnergyThreshold_upper) + { + // the predicted energy state is high so an extra load may need to be added + boolean OK_toAddLoad = true; // default state of flag + + byte tempLoad = nextLogicalLoadToBeAdded(); + if (tempLoad < noOfDumploads) + { + // a load which is now OFF has been identified for potentially being switched ON + if (recentTransition) + { + if (tempLoad != activeLoad) + { + OK_toAddLoad = false; + } + } + + if (OK_toAddLoad) + { + // tempLoad will be either the active load, or the next load in the priority sequence + logicalLoadState[tempLoad] = LOAD_ON; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + } + } + } + else + if (energyInBucket_prediction < workingEnergyThreshold_lower) + { + // the predicted energy state is low so some load may need to be removed + boolean OK_toRemoveLoad = true; // default state of flag + + byte tempLoad = nextLogicalLoadToBeRemoved(); + if (tempLoad < noOfDumploads) + { + // a load which is now ON has been identified for potentially being switched OFF + if (recentTransition) + { + if (tempLoad != activeLoad) + { + OK_toRemoveLoad = false; + } + } + + if (OK_toRemoveLoad) + { + // tempLoad will be either the active load, or the next load in the priority sequence + logicalLoadState[tempLoad] = LOAD_OFF; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + } + } + } + + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + // update each of the physical loads + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // active high for additional load + + // update the Energy Diversion Detector + if (physicalLoadState[0] == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + } + } + + sampleSetsDuringNegativeHalfOfMainsCycle++; + } // end of processing that is specific to samples where the voltage is negative + + // processing for EVERY set of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + // + // extra filtering to offset the HPF effect of CT1 + long last_lpf_long = lpf_long; + lpf_long = last_lpf_long + alpha *(sampleIminusDC_grid - last_lpf_long); + sampleIminusDC_grid += (lpf_gain * lpf_long); + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (x4096, or 2^12) + instP = instP>>12; // reduce to 20-bits (x1) + sumP_grid +=instP; // scaling is x1 + // + instP = instP>>4; // reduce to 16-bits (x1/16, or 2^-4) for more datalog range + sumP_atSupplyPoint +=instP; // scaling is x1/16, for datalogging + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (x4096, or 2^12) + instP = instP>>12; // reduce to 20-bits (x1) + sumP_diverted +=instP; // scaling is x1 + // + instP = instP>>4; // reduce to 16-bits (x1/16, or 2^-4) for more datalog range + sumP_forDivertedPower +=instP; // scaling is x1/16, for datalogging + + // for the Vrms calculation (for datalogging only) + long inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (now x4096, or 2^12) +// inst_Vsquared = inst_Vsquared>>12; // 20-bits (x1), not enough range :-( + inst_Vsquared = inst_Vsquared>>16; // 16-bits (x1/16, or 2^-4), for more datalog range + sum_Vsquared += inst_Vsquared; // scaling is x1/16 + + sampleSetsDuringThisMainsCycle++; + samplesDuringDatalogPeriod++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + + refreshDisplay(); +} + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count > PERSISTENCE_FOR_POLARITY_CHANGE) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + +byte nextLogicalLoadToBeAdded() +{ + byte retVal = noOfDumploads; + boolean success = false; + for (byte index = 0; index < noOfDumploads && !success; index++) + { + if (logicalLoadState[index] == LOAD_OFF) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +byte nextLogicalLoadToBeRemoved() +{ + byte retVal = noOfDumploads; + boolean success = false; + + // this index counter can't be a 'byte' because the loop would run forever! + // + for (int index = (noOfDumploads -1); index >= 0 && !success; index--) + { + if (logicalLoadState[index] == LOAD_ON) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * The association between the physical and logical loads is 1:1. By default, numerical + * equivalence is maintained, so logical(N) maps to physical(N). If physical load 1 is set + * to have priority, rather than physical load 0, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * Any other mapping relaionships could be configured here. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } +} + + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + +// Serial.println(divertedEnergyTotal_Wh); + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +} + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + // The two versions of the display hardware require different logic. + + // With the pin-saving hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + +#ifdef RF_PRESENT +void send_rf_data() +{ + // rf12_sleep(RF12_WAKEUP); + // if ready to send + exit route if it gets stuck + int i = 0; + while (!rf12_canSend() && i<10) + { + rf12_recvDone(); + i++; + } + rf12_sendNow(0, &tx_data, sizeof tx_data); + + // rf12_sendStart(0, &tx_data, sizeof tx_data); + // rf12_sendWait(2); + // rf12_sleep(RF12_SLEEP); +} +#endif + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_twoLoads_1.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_twoLoads_1.ino new file mode 100644 index 0000000..adb84b1 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_twoLoads_1.ino @@ -0,0 +1,1137 @@ +/* Mk2_fasterControl_twoLoads_1.ino + * + * (initially released as Mk2_bothDisplays_1 in March 2014) + * This sketch is for diverting suplus PV power to a dump load using a triac or + * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on + * the OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * In this latest version, the pin-allocations have been changed to suit my + * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is + * fed from one of the secondary coils of the transformer. Current is measured + * via Current Transformers at the CT1 and CT1 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. This can be driven in two + * different ways, one with an extra pair of logic chips, and one without. The + * appropriate version of the sketch must be selected by including or commenting + * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code. + * + * September 2014: renamed as Mk2_bothDisplays_2, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - removal of unhelpful comments in the IO pin section; + * - tidier initialisation of display logic in setup(); + * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility); + * + * December 2014: renamed as Mk2_bothDisplays_3, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed) + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances + * + * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed. + * + * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to + * remove a timing uncertainty. + * + * January 2016: updated to Mk2_bothDisplays_3c: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * February 2016: updated to Mk2_bothDisplays_4, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * November 2019: updated to Mk2_fasterControl_1 with these changes: + * - Half way through each mains cycle, a prediction is made of the likely energy level at the + * end of the cycle. That predicted value allows the triac to be switched at the +ve going + * zero-crossing point rather than waiting for a further 10 ms. These changes allow for + * faster switching of the load. + * - The range of the energy bucket has been reduced to one tenth of its former value. This + * allows the unit's operation to commence more rapidly whenever surplus power is available. + * - controlMode is no longer selectable, the unit's operation being effectively hard-coded + * as "Normal" rather than Anti-flicker. + * - Port D3 now supports an indicator which shows when the level in the energy bucket + * reaches either end of its range. While the unit is actively diverting surplus power, + * it is vital that the level in the reduced capacity energy bucket remains within its + * permitted range, hence the addition of this indicator. + * + * February 20120: updated to Mk2_fasterControl_twoLoads_1 with these changes: + * - the energy overflow indicator has been disabled to free up port D3 + * - port D3 now supports a second load + * + * * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define WORKING_RANGE_IN_JOULES 360 // 0.1 Wh, reduced for faster start-up +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +// The two versions of the hardware require different logic. The following line should +// be included if the additional logic chips are present, or excluded if they are +// absent (in which case some wire links need to be fitted) +// +#define PIN_SAVING_HARDWARE + +const byte noOfDumploads = 2; + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are logically active-low +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +// For this go-faster version, the unit's operation will effectively always be "Normal"; +// there is no "Anti-flicker" option. The controlMode variable has been removed. + +// allocation of digital pins which are not dependent on the display type that is in use +// ************************************************************************************* +// const byte outOfRangeIndication = 3; // <-- this output port is active-high +const byte physicalLoad_1_pin = 3; // <-- the "mode" port is active-high +const byte physicalLoad_0_pin = 4; // <-- the "trigger" port is active-low + +// allocation of analogue pins which are not dependent on the display type that is in use +// ************************************************************************************** +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + +const byte delayBeforeSerialStarts = 1; // in seconds, to allow Serial window to be opened +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long singleEnergyThreshold_long; // for go-faster use + +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; + +#define POST_TRANSITION_MAX_COUNT 3 // <-- allows each transition to take effect + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +volatile int sampleI_grid; +volatile int sampleI_diverted; +volatile int sampleV; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define PERSISTENCE_FOR_POLARITY_CHANGE 1 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "RawSamplesTool_2chan.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + +// for this go-faster sketch, the phaseCal logic has been removed. If required, it can be +// found in most of the standard Mk2_bothDisplay_n versions + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define UPDATE_PERIOD_FOR_DISPLAYED_DATA 50 // mains cycles +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +// The two versions of the hardware require different logic. +#ifdef PIN_SAVING_HARDWARE + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. In this version, +// the decimal point has to be treated differently than the other seven segments, so +// a convenient means of accessing this column is provided. +// +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + +#else // PIN_SAVING_HARDWARE + +#define ON HIGH +#define OFF LOW + +const byte noOfSegmentsPerDigit = 8; // includes one for the decimal point +enum digitEnableStates {DIGIT_ENABLED, DIGIT_DISABLED}; + +byte digitSelectorPin[noOfDigitLocations] = {16,10,13,11}; +byte segmentDrivePin[noOfSegmentsPerDigit] = {2,5,12,6,7,9,8,14}; + +// The final column of segMap[] is for the decimal point status. In this version, +// the decimal point is treated just like all the other segments, so there is +// no need to access this column specifically. +// +byte segMap[noOfPossibleCharacters][noOfSegmentsPerDigit] = { + ON , ON , ON , ON , ON , ON , OFF, OFF, // '0' <- element 0 + OFF, ON , ON , OFF, OFF, OFF, OFF, OFF, // '1' <- element 1 + ON , ON , OFF, ON , ON , OFF, ON , OFF, // '2' <- element 2 + ON , ON , ON , ON , OFF, OFF, ON , OFF, // '3' <- element 3 + OFF, ON , ON , OFF, OFF, ON , ON , OFF, // '4' <- element 4 + ON , OFF, ON , ON , OFF, ON , ON , OFF, // '5' <- element 5 + ON , OFF, ON , ON , ON , ON , ON , OFF, // '6' <- element 6 + ON , ON , ON , OFF, OFF, OFF, OFF, OFF, // '7' <- element 7 + ON , ON , ON , ON , ON , ON , ON , OFF, // '8' <- element 8 + ON , ON , ON , ON , OFF, ON , ON , OFF, // '9' <- element 9 + ON , ON , ON , ON , ON , ON , OFF, ON , // '0.' <- element 10 + OFF, ON , ON , OFF, OFF, OFF, OFF, ON , // '1.' <- element 11 + ON , ON , OFF, ON , ON , OFF, ON , ON , // '2.' <- element 12 + ON , ON , ON , ON , OFF, OFF, ON , ON , // '3.' <- element 13 + OFF, ON , ON , OFF, OFF, ON , ON , ON , // '4.' <- element 14 + ON , OFF, ON , ON , OFF, ON , ON , ON , // '5.' <- element 15 + ON , OFF, ON , ON , ON , ON , ON , ON , // '6.' <- element 16 + ON , ON , ON , OFF, OFF, OFF, OFF, ON , // '7.' <- element 17 + ON , ON , ON , ON , ON , ON , ON , ON , // '8.' <- element 18 + ON , ON , ON , ON , OFF, ON , ON , ON , // '9.' <- element 19 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, // ' ' <- element 20 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, ON // '.' <- element 11 +}; +#endif // PIN_SAVING_HARDWARE + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // energy divertion detection +long requiredExportPerMainsCycle_inIEU; + + +void setup() +{ +// pinMode(outOfRangeIndication, OUTPUT); +// digitalWrite (outOfRangeIndication, LED_OFF); + + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for the primary load + pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for an additional load + // + for(int i = 0; i< noOfDumploads; i++) // re-using the logic from my multiLoad code + { + logicalLoadState[i] = LOAD_OFF; + physicalLoadState[i] = LOAD_OFF; + } + // + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // the primary load is active low. + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // the additional load is active high. + + delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_fasterControl_twoLoads_1.ino"); + Serial.println(); + +#ifdef PIN_SAVING_HARDWARE + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } +#else + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + pinMode(segmentDrivePin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + pinMode(digitSelectorPin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + digitalWrite(digitSelectorPin[i], DIGIT_DISABLED); } + + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + digitalWrite(segmentDrivePin[i], OFF); } +#endif + + + + // When using integer maths, calibration values that have supplied in floating point + // form need to be rescaled. + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + energyInBucket_long = 0; + + singleEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + + Serial.println ("----"); +} + +// An Interrupt Service Routine is now defined in which the ADC is instructed to +// measure each analogue input in sequence. A "data ready" flag is set after each +// voltage conversion has been completed. +// For each set of samples, the two samples for current are taken before the one +// for voltage. This is appropriate because each waveform current is generally slightly +// advanced relative to the waveform for voltage. The data ready flag is cleared +// within loop(). +// This Interrupt Service Routine is for use when the ADC is fixed timer mode. It is +// executed whenever the ADC timer expires. In this mode, the next ADC conversion is +// initiated from within this ISR. +// +void timerIsr(void) +{ + static unsigned char sample_index = 0; + static int sampleI_grid_raw; + static int sampleI_diverted_raw; + + + switch(sample_index) + { + case 0: + sampleV = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current + ADCSRA |= (1< 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + if (beyondStartUpPhase) + { + // a simple routine for checking the performance of this new ISR structure + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / sampleSetsDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step would normally be to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient to just + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + bool endOfRangeEncountered = false; + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; + endOfRangeEncountered = true;} + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; + endOfRangeEncountered = true;} + +/* + if (endOfRangeEncountered) { + digitalWrite (outOfRangeIndication , LED_ON); } + else { + digitalWrite (outOfRangeIndication , LED_OFF); } +*/ + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. + + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) + { + realEnergy_diverted = 0; + } + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + if(timerForDisplayUpdate > UPDATE_PERIOD_FOR_DISPLAYED_DATA) + { // the 4-digit display needs to be refreshed every few mS. For convenience, + // this action is performed every N times around this processing loop. + timerForDisplayUpdate = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); + } + else + { + timerForDisplayUpdate++; + } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringNegativeHalfOfMainsCycle = 0; + + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringThisMainsCycle = 0; // not yet dealt with for this cycle + sampleCount_forContinuityChecker = 1; // opportunity has been missed for this cycle + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + // (in this go-faster code, the action from here has moved to the negative half of the cycle) + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12); + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + // The average power that has been measured during the first half of this mains cycle can now be used + // to predict the energy state at the end of this mains cycle. That prediction will be used to alter + // the state of the load as necessary. The arming signal for the triac can't be set yet - that must + // wait until the voltage has advanced further beyond the -ve going zero-crossing point. + // + long averagePower = sumP_grid / sampleSetsDuringThisMainsCycle;// for 1st half of this mains cycle only + // + // To avoid repetitive and unnecessary calculations, the increase in energy during each mains cycle is + // deemed to be numerically equal to the average power. The predicted value for the energy state at the + // end of this mains cycle will therefore be the known energy state at its start plus the average power + // as measured. Although the average power has been determined over only half a mains cycle, the correct + // number of contributing sample sets has been used so the result can be expected to be a true measurement + // of average power, not half of it. + // However, it can be shown that the average power during the first half of any mains cycle after the + // load has changed state will alway be under-recorded so its value should now be increased by 30%. This + // arbitrary looking adjustment gives good test results with differening amounts of surplus power and it + // only affects the predicted value of the energy state at the end of the current mains cycle; it does + // not affect the value in the main energy bucket. This complication is a fundamental limitation + // of the floating CTs that we use. + // + if (loadHasJustChangedState) + { + averagePower = averagePower * 1.3; + } + energyInBucket_prediction = energyInBucket_long + averagePower; // at end of this mains cycle + + + } // end of processing that is specific to the first Vsample in each -ve half cycle + + // check to see whether the trigger device can now be reliably armed + if (sampleSetsDuringNegativeHalfOfMainsCycle == 3) + { + if (beyondStartUpPhase) + { + /* Determining whether any of the loads need to be changed is is a 3-stage process: + * - change the LOGICAL load states as necessary to maintain the energy level + * - update the PHYSICAL load states according to the logical -> physical mapping + * - update the driver lines for each of the loads. + */ + // Restrictions apply for the period immediately after a load has been switched. + // Here the recentTransition flag is checked and updated as necessary. + if (recentTransition) + { + postTransitionCount++; + if (postTransitionCount >= POST_TRANSITION_MAX_COUNT) + { + recentTransition = false; + } + } + + loadHasJustChangedState = false; // flag is cleared by default + if (energyInBucket_prediction > singleEnergyThreshold_long) + { + // the energy state is in the upper half of the working range + boolean OK_toAddLoad = true; + byte tempLoad = nextLogicalLoadToBeAdded(); + if (tempLoad < noOfDumploads) + { + // a load which is now OFF has been identified for potentially being switched ON + if (recentTransition) + { + if (tempLoad != activeLoad) + { + OK_toAddLoad = false; + } + } + + if (OK_toAddLoad) + { + logicalLoadState[tempLoad] = LOAD_ON; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + loadHasJustChangedState = true; + } + } + } + else + { // the energy state is in the lower half of the working range + boolean OK_toRemoveLoad = true; + byte tempLoad = nextLogicalLoadToBeRemoved(); + if (tempLoad < noOfDumploads) + { + // a load which is now ON has been identified for potentially being switched OFF + if (recentTransition) + { + if (tempLoad != activeLoad) + { + OK_toRemoveLoad = false; + } + } + + if (OK_toRemoveLoad) + { + logicalLoadState[tempLoad] = LOAD_OFF; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + loadHasJustChangedState = true; + } + } + } + + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + // update each of the physical loads + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // active high for additional load + + // update the Energy Diversion Detector + if (physicalLoadState[0] == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + } + } + + sampleSetsDuringNegativeHalfOfMainsCycle++; + } // end of processing that is specific to samples where the voltage is negative + + // processing for EVERY set of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + sampleSetsDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count > PERSISTENCE_FOR_POLARITY_CHANGE) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + +byte nextLogicalLoadToBeAdded() +{ + byte retVal = noOfDumploads; + boolean success = false; + for (byte index = 0; index < noOfDumploads && !success; index++) + { + if (logicalLoadState[index] == LOAD_OFF) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +byte nextLogicalLoadToBeRemoved() +{ + byte retVal = noOfDumploads; + boolean success = false; + + // this index counter can't be a 'byte' because the loop would run forever! + // + for (int index = (noOfDumploads -1); index >= 0 && !success; index--) + { + if (logicalLoadState[index] == LOAD_ON) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * The association between the physical and logical loads is 1:1. By default, numerical + * equivalence is maintained, so logical(N) maps to physical(N). If physical load 1 is set + * to have priority, rather than physical load 0, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * Any other mapping relaionships could be configured here. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } +} + + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + +// Serial.println(divertedEnergyTotal_Wh); + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +} + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + // The two versions of the hardware require different logic. + +#ifdef PIN_SAVING_HARDWARE + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } + +#else // PIN_SAVING_HARDWARE + + // This version is more straightforward because the digit-enable lines can be + // used to mask out all of the transitory states, including the Decimal Point. + // The sequence is: + // + // 1. de-activate the digit-enable line that was previously active + // 2. determine the next location which is to be active + // 3. determine the relevant character for the new active location + // 4. set up the segment drivers for the character to be displayed (includes the DP) + // 5. activate the digit-enable line for the new active location + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + displayTime_count = 0; + + // 1. de-activate the location which is currently being displayed + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_DISABLED); + + // 2. determine the next digit location which is to be displayed + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 3. determine the relevant character for the new active location + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 4. set up the segment drivers for the character to be displayed (includes the DP) + for (byte segment = 0; segment < noOfSegmentsPerDigit; segment++) { + byte segmentState = segMap[digitVal][segment]; + digitalWrite( segmentDrivePin[segment], segmentState); } + + // 5. activate the digit-enable line for the new active location + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_ENABLED); + } +#endif // PIN_SAVING_HARDWARE + +} // end of refreshDisplay() + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_twoLoads_2.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_twoLoads_2.ino new file mode 100644 index 0000000..5912b78 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_twoLoads_2.ino @@ -0,0 +1,1184 @@ + +/* Mk2_fasterControl_twoLoads_2.ino + * + * (initially released as Mk2_bothDisplays_1 in March 2014) + * This sketch is for diverting suplus PV power to a dump load using a triac or + * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on + * the OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * In this latest version, the pin-allocations have been changed to suit my + * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is + * fed from one of the secondary coils of the transformer. Current is measured + * via Current Transformers at the CT1 and CT1 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. This can be driven in two + * different ways, one with an extra pair of logic chips, and one without. The + * appropriate version of the sketch must be selected by including or commenting + * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code. + * + * September 2014: renamed as Mk2_bothDisplays_2, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - removal of unhelpful comments in the IO pin section; + * - tidier initialisation of display logic in setup(); + * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility); + * + * December 2014: renamed as Mk2_bothDisplays_3, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed) + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances + * + * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed. + * + * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to + * remove a timing uncertainty. + * + * January 2016: updated to Mk2_bothDisplays_3c: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * February 2016: updated to Mk2_bothDisplays_4, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * November 2019: updated to Mk2_fasterControl_1 with these changes: + * - Half way through each mains cycle, a prediction is made of the likely energy level at the + * end of the cycle. That predicted value allows the triac to be switched at the +ve going + * zero-crossing point rather than waiting for a further 10 ms. These changes allow for + * faster switching of the load. + * - The range of the energy bucket has been reduced to one tenth of its former value. This + * allows the unit's operation to commence more rapidly whenever surplus power is available. + * - controlMode is no longer selectable, the unit's operation being effectively hard-coded + * as "Normal" rather than Anti-flicker. + * - Port D3 now supports an indicator which shows when the level in the energy bucket + * reaches either end of its range. While the unit is actively diverting surplus power, + * it is vital that the level in the reduced capacity energy bucket remains within its + * permitted range, hence the addition of this indicator. + * + * February 2020: updated to Mk2_fasterControl_twoLoads_1 with these changes: + * - the energy overflow indicator has been disabled to free up port D3 + * - port D3 now supports a second load + * + * February 2020: updated to Mk2_fasterControl_twoLoads_2 with these changes: + * - improved multi-load control logic to prevent the primary load from being disturbed by + * the lower priority one. This logic now mirrors that in the Mk2_multiLoad_wired_n line. + * + * + * * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define WORKING_RANGE_IN_JOULES 360 // 0.1 Wh, reduced for faster start-up +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +// The two versions of the hardware require different logic. The following line should +// be included if the additional logic chips are present, or excluded if they are +// absent (in which case some wire links need to be fitted) +// +#define PIN_SAVING_HARDWARE + +const byte noOfDumploads = 2; + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are logically active-low +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +// For this go-faster version, the unit's operation will effectively always be "Normal"; +// there is no "Anti-flicker" option. The controlMode variable has been removed. + +// allocation of digital pins which are not dependent on the display type that is in use +// ************************************************************************************* +// const byte outOfRangeIndication = 3; // <-- this output port is active-high +const byte physicalLoad_1_pin = 3; // <-- the "mode" port is active-high +const byte physicalLoad_0_pin = 4; // <-- the "trigger" port is active-low + +// allocation of analogue pins which are not dependent on the display type that is in use +// ************************************************************************************** +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + +const byte delayBeforeSerialStarts = 1; // in seconds, to allow Serial window to be opened +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long nominalEnergyThreshold; +long workingEnergyThreshold_upper; +long workingEnergyThreshold_lower; + +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; + +#define POST_TRANSITION_MAX_COUNT 3 // <-- allows each transition to take effect + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +volatile int sampleI_grid; +volatile int sampleI_diverted; +volatile int sampleV; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define PERSISTENCE_FOR_POLARITY_CHANGE 1 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "RawSamplesTool_2chan.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + +// for this go-faster sketch, the phaseCal logic has been removed. If required, it can be +// found in most of the standard Mk2_bothDisplay_n versions + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define UPDATE_PERIOD_FOR_DISPLAYED_DATA 50 // mains cycles +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +// The two versions of the hardware require different logic. +#ifdef PIN_SAVING_HARDWARE + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. In this version, +// the decimal point has to be treated differently than the other seven segments, so +// a convenient means of accessing this column is provided. +// +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + +#else // PIN_SAVING_HARDWARE + +#define ON HIGH +#define OFF LOW + +const byte noOfSegmentsPerDigit = 8; // includes one for the decimal point +enum digitEnableStates {DIGIT_ENABLED, DIGIT_DISABLED}; + +byte digitSelectorPin[noOfDigitLocations] = {16,10,13,11}; +byte segmentDrivePin[noOfSegmentsPerDigit] = {2,5,12,6,7,9,8,14}; + +// The final column of segMap[] is for the decimal point status. In this version, +// the decimal point is treated just like all the other segments, so there is +// no need to access this column specifically. +// +byte segMap[noOfPossibleCharacters][noOfSegmentsPerDigit] = { + ON , ON , ON , ON , ON , ON , OFF, OFF, // '0' <- element 0 + OFF, ON , ON , OFF, OFF, OFF, OFF, OFF, // '1' <- element 1 + ON , ON , OFF, ON , ON , OFF, ON , OFF, // '2' <- element 2 + ON , ON , ON , ON , OFF, OFF, ON , OFF, // '3' <- element 3 + OFF, ON , ON , OFF, OFF, ON , ON , OFF, // '4' <- element 4 + ON , OFF, ON , ON , OFF, ON , ON , OFF, // '5' <- element 5 + ON , OFF, ON , ON , ON , ON , ON , OFF, // '6' <- element 6 + ON , ON , ON , OFF, OFF, OFF, OFF, OFF, // '7' <- element 7 + ON , ON , ON , ON , ON , ON , ON , OFF, // '8' <- element 8 + ON , ON , ON , ON , OFF, ON , ON , OFF, // '9' <- element 9 + ON , ON , ON , ON , ON , ON , OFF, ON , // '0.' <- element 10 + OFF, ON , ON , OFF, OFF, OFF, OFF, ON , // '1.' <- element 11 + ON , ON , OFF, ON , ON , OFF, ON , ON , // '2.' <- element 12 + ON , ON , ON , ON , OFF, OFF, ON , ON , // '3.' <- element 13 + OFF, ON , ON , OFF, OFF, ON , ON , ON , // '4.' <- element 14 + ON , OFF, ON , ON , OFF, ON , ON , ON , // '5.' <- element 15 + ON , OFF, ON , ON , ON , ON , ON , ON , // '6.' <- element 16 + ON , ON , ON , OFF, OFF, OFF, OFF, ON , // '7.' <- element 17 + ON , ON , ON , ON , ON , ON , ON , ON , // '8.' <- element 18 + ON , ON , ON , ON , OFF, ON , ON , ON , // '9.' <- element 19 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, // ' ' <- element 20 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, ON // '.' <- element 11 +}; +#endif // PIN_SAVING_HARDWARE + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // energy divertion detection +long requiredExportPerMainsCycle_inIEU; + + +void setup() +{ +// pinMode(outOfRangeIndication, OUTPUT); +// digitalWrite (outOfRangeIndication, LED_OFF); + + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for the primary load + pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for an additional load + // + for(int i = 0; i< noOfDumploads; i++) // re-using the logic from my multiLoad code + { + logicalLoadState[i] = LOAD_OFF; + physicalLoadState[i] = LOAD_OFF; + } + // + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // the primary load is active low. + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // the additional load is active high. + + delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_fasterControl_twoLoads_2.ino"); + Serial.println(); + +#ifdef PIN_SAVING_HARDWARE + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } +#else + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + pinMode(segmentDrivePin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + pinMode(digitSelectorPin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + digitalWrite(digitSelectorPin[i], DIGIT_DISABLED); } + + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + digitalWrite(segmentDrivePin[i], OFF); } +#endif + + + + // When using integer maths, calibration values that have supplied in floating point + // form need to be rescaled. + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + energyInBucket_long = 0; + + nominalEnergyThreshold = capacityOfEnergyBucket_long * 0.5; + workingEnergyThreshold_upper = nominalEnergyThreshold; // initial value + workingEnergyThreshold_lower = nominalEnergyThreshold; // initial value + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + + Serial.println ("----"); +} + +// An Interrupt Service Routine is now defined in which the ADC is instructed to +// measure each analogue input in sequence. A "data ready" flag is set after each +// voltage conversion has been completed. +// For each set of samples, the two samples for current are taken before the one +// for voltage. This is appropriate because each waveform current is generally slightly +// advanced relative to the waveform for voltage. The data ready flag is cleared +// within loop(). +// This Interrupt Service Routine is for use when the ADC is fixed timer mode. It is +// executed whenever the ADC timer expires. In this mode, the next ADC conversion is +// initiated from within this ISR. +// +void timerIsr(void) +{ + static unsigned char sample_index = 0; + static int sampleI_grid_raw; + static int sampleI_diverted_raw; + + + switch(sample_index) + { + case 0: + sampleV = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current + ADCSRA |= (1< 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + if (beyondStartUpPhase) + { + // a simple routine for checking the performance of this new ISR structure + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / sampleSetsDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step would normally be to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient to just + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + bool endOfRangeEncountered = false; + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; + endOfRangeEncountered = true;} + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; + endOfRangeEncountered = true;} + +/* + if (endOfRangeEncountered) { + digitalWrite (outOfRangeIndication , LED_ON); } + else { + digitalWrite (outOfRangeIndication , LED_OFF); } +*/ + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. + + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) + { + realEnergy_diverted = 0; + } + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + if(timerForDisplayUpdate > UPDATE_PERIOD_FOR_DISPLAYED_DATA) + { // the 4-digit display needs to be refreshed every few mS. For convenience, + // this action is performed every N times around this processing loop. + timerForDisplayUpdate = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); + } + else + { + timerForDisplayUpdate++; + } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringNegativeHalfOfMainsCycle = 0; + + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringThisMainsCycle = 0; // not yet dealt with for this cycle + sampleCount_forContinuityChecker = 1; // opportunity has been missed for this cycle + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + if(sampleSetsDuringThisMainsCycle == 5) // to distribute the workload within each mains cycle + { + if (beyondStartUpPhase) + { + // Restrictions apply for the period immediately after a load has been switched. + // Here the recentTransition flag is checked and updated as necessary. + if (recentTransition) + { + postTransitionCount++; + if (postTransitionCount >= POST_TRANSITION_MAX_COUNT) + { + recentTransition = false; + } + } + + if (recentTransition) + { + // During the post-transition period, any change in the energy level is noted. + if (energyInBucket_long > workingEnergyThreshold_upper) + { + workingEnergyThreshold_lower = nominalEnergyThreshold; // reset the opposite threshold + workingEnergyThreshold_upper = energyInBucket_long; + + // the energy thresholds must remain within range + if (workingEnergyThreshold_upper > capacityOfEnergyBucket_long) + { + workingEnergyThreshold_upper = capacityOfEnergyBucket_long; + } + } + else + if (energyInBucket_long < workingEnergyThreshold_lower) + { + workingEnergyThreshold_upper = nominalEnergyThreshold; // reset the opposite threshold + workingEnergyThreshold_lower = energyInBucket_long; + + // the energy thresholds must remain within range + if (workingEnergyThreshold_lower < 0) + { + workingEnergyThreshold_lower = 0; + } + } + } + } + } + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12); + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + // The average power that has been measured during the first half of this mains cycle can now be used + // to predict the energy state at the end of this mains cycle. That prediction will be used to alter + // the state of the load as necessary. The arming signal for the triac can't be set yet - that must + // wait until the voltage has advanced further beyond the -ve going zero-crossing point. + // + long averagePower = sumP_grid / sampleSetsDuringThisMainsCycle;// for 1st half of this mains cycle only + // + // To avoid repetitive and unnecessary calculations, the increase in energy during each mains cycle is + // deemed to be numerically equal to the average power. The predicted value for the energy state at the + // end of this mains cycle will therefore be the known energy state at its start plus the average power + // as measured. Although the average power has been determined over only half a mains cycle, the correct + // number of contributing sample sets has been used so the result can be expected to be a true measurement + // of average power, not half of it. + // However, it can be shown that the average power during the first half of any mains cycle after the + // load has changed state will alway be under-recorded so its value should now be increased by 30%. This + // arbitrary looking adjustment gives good test results with differening amounts of surplus power and it + // only affects the predicted value of the energy state at the end of the current mains cycle; it does + // not affect the value in the main energy bucket. This complication is a fundamental consequence + // of the floating CTs that we use. + // + if (loadHasJustChangedState) + { + averagePower = averagePower * 1.3; + } + energyInBucket_prediction = energyInBucket_long + averagePower; // at end of this mains cycle + + + } // end of processing that is specific to the first Vsample in each -ve half cycle + + if (sampleSetsDuringNegativeHalfOfMainsCycle == 5) + { + // the zero-crossing trigger device(s) can now be reliably armed + if (beyondStartUpPhase) + { + /* Determining whether any of the loads need to be changed is is a 3-stage process: + * - change the LOGICAL load states as necessary to maintain the energy level + * - update the PHYSICAL load states according to the logical -> physical mapping + * - update the driver lines for each of the loads. + */ + + loadHasJustChangedState = false; // clear the predictive algorithm's flag + if (energyInBucket_prediction > workingEnergyThreshold_upper) + { + // the predicted energy state is high so an extra load may need to be added + boolean OK_toAddLoad = true; // default state of flag + + byte tempLoad = nextLogicalLoadToBeAdded(); + if (tempLoad < noOfDumploads) + { + // a load which is now OFF has been identified for potentially being switched ON + if (recentTransition) + { + if (tempLoad != activeLoad) + { + OK_toAddLoad = false; + } + } + + if (OK_toAddLoad) + { + // tempLoad will be either the active load, or the next load in the priority sequence + logicalLoadState[tempLoad] = LOAD_ON; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + loadHasJustChangedState = true; + } + } + } + else + if (energyInBucket_prediction < workingEnergyThreshold_lower) + { + // the predicted energy state is low so some load may need to be removed + boolean OK_toRemoveLoad = true; // default state of flag + + byte tempLoad = nextLogicalLoadToBeRemoved(); + if (tempLoad < noOfDumploads) + { + // a load which is now ON has been identified for potentially being switched OFF + if (recentTransition) + { + if (tempLoad != activeLoad) + { + OK_toRemoveLoad = false; + } + } + + if (OK_toRemoveLoad) + { + // tempLoad will be either the active load, or the next load in the priority sequence + logicalLoadState[tempLoad] = LOAD_OFF; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + loadHasJustChangedState = true; + } + } + } + + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + // update each of the physical loads + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // active high for additional load + + // update the Energy Diversion Detector + if (physicalLoadState[0] == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + } + } + + sampleSetsDuringNegativeHalfOfMainsCycle++; + } // end of processing that is specific to samples where the voltage is negative + + // processing for EVERY set of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + sampleSetsDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count > PERSISTENCE_FOR_POLARITY_CHANGE) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + +byte nextLogicalLoadToBeAdded() +{ + byte retVal = noOfDumploads; + boolean success = false; + for (byte index = 0; index < noOfDumploads && !success; index++) + { + if (logicalLoadState[index] == LOAD_OFF) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +byte nextLogicalLoadToBeRemoved() +{ + byte retVal = noOfDumploads; + boolean success = false; + + // this index counter can't be a 'byte' because the loop would run forever! + // + for (int index = (noOfDumploads -1); index >= 0 && !success; index--) + { + if (logicalLoadState[index] == LOAD_ON) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * The association between the physical and logical loads is 1:1. By default, numerical + * equivalence is maintained, so logical(N) maps to physical(N). If physical load 1 is set + * to have priority, rather than physical load 0, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * Any other mapping relaionships could be configured here. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } +} + + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + +// Serial.println(divertedEnergyTotal_Wh); + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +} + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + // The two versions of the hardware require different logic. + +#ifdef PIN_SAVING_HARDWARE + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } + +#else // PIN_SAVING_HARDWARE + + // This version is more straightforward because the digit-enable lines can be + // used to mask out all of the transitory states, including the Decimal Point. + // The sequence is: + // + // 1. de-activate the digit-enable line that was previously active + // 2. determine the next location which is to be active + // 3. determine the relevant character for the new active location + // 4. set up the segment drivers for the character to be displayed (includes the DP) + // 5. activate the digit-enable line for the new active location + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + displayTime_count = 0; + + // 1. de-activate the location which is currently being displayed + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_DISABLED); + + // 2. determine the next digit location which is to be displayed + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 3. determine the relevant character for the new active location + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 4. set up the segment drivers for the character to be displayed (includes the DP) + for (byte segment = 0; segment < noOfSegmentsPerDigit; segment++) { + byte segmentState = segMap[digitVal][segment]; + digitalWrite( segmentDrivePin[segment], segmentState); } + + // 5. activate the digit-enable line for the new active location + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_ENABLED); + } +#endif // PIN_SAVING_HARDWARE + +} // end of refreshDisplay() + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_twoLoads_3.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_twoLoads_3.ino new file mode 100644 index 0000000..8979efd --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_twoLoads_3.ino @@ -0,0 +1,1190 @@ + +/* Mk2_fasterControl_twoLoads_3.ino + * + * (initially released as Mk2_bothDisplays_1 in March 2014) + * This sketch is for diverting suplus PV power to a dump load using a triac or + * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on + * the OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * In this latest version, the pin-allocations have been changed to suit my + * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is + * fed from one of the secondary coils of the transformer. Current is measured + * via Current Transformers at the CT1 and CT1 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. This can be driven in two + * different ways, one with an extra pair of logic chips, and one without. The + * appropriate version of the sketch must be selected by including or commenting + * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code. + * + * September 2014: renamed as Mk2_bothDisplays_2, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - removal of unhelpful comments in the IO pin section; + * - tidier initialisation of display logic in setup(); + * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility); + * + * December 2014: renamed as Mk2_bothDisplays_3, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed) + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances + * + * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed. + * + * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to + * remove a timing uncertainty. + * + * January 2016: updated to Mk2_bothDisplays_3c: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * February 2016: updated to Mk2_bothDisplays_4, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * November 2019: updated to Mk2_fasterControl_1 with these changes: + * - Half way through each mains cycle, a prediction is made of the likely energy level at the + * end of the cycle. That predicted value allows the triac to be switched at the +ve going + * zero-crossing point rather than waiting for a further 10 ms. These changes allow for + * faster switching of the load. + * - The range of the energy bucket has been reduced to one tenth of its former value. This + * allows the unit's operation to commence more rapidly whenever surplus power is available. + * - controlMode is no longer selectable, the unit's operation being effectively hard-coded + * as "Normal" rather than Anti-flicker. + * - Port D3 now supports an indicator which shows when the level in the energy bucket + * reaches either end of its range. While the unit is actively diverting surplus power, + * it is vital that the level in the reduced capacity energy bucket remains within its + * permitted range, hence the addition of this indicator. + * + * February 2020: updated to Mk2_fasterControl_twoLoads_1 with these changes: + * - the energy overflow indicator has been disabled to free up port D3 + * - port D3 now supports a second load + * + * February 2020: updated to Mk2_fasterControl_twoLoads_2 with these changes: + * - improved multi-load control logic to prevent the primary load from being disturbed by + * the lower priority one. This logic now mirrors that in the Mk2_multiLoad_wired_n line. + * + * March 2021: updated to Mk2_fasterControl_twoLoads_3 with these changes: + * - extra filtering added to offset the HPF effect of CT1. This allows the energy state in + * 10 ms time to be predicted with more confidence. Specifically, it is no longer necessary + * to include a 30% boost factor after each change of load state. + * + * + * * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define WORKING_RANGE_IN_JOULES 360 // 0.1 Wh, reduced for faster start-up +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +// The two versions of the hardware require different logic. The following line should +// be included if the additional logic chips are present, or excluded if they are +// absent (in which case some wire links need to be fitted) +// +#define PIN_SAVING_HARDWARE + +const byte noOfDumploads = 2; + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are logically active-low +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +// For this go-faster version, the unit's operation will effectively always be "Normal"; +// there is no "Anti-flicker" option. The controlMode variable has been removed. + +// allocation of digital pins which are not dependent on the display type that is in use +// ************************************************************************************* +// const byte outOfRangeIndication = 3; // <-- this output port is active-high +const byte physicalLoad_1_pin = 3; // <-- the "mode" port is active-high +const byte physicalLoad_0_pin = 4; // <-- the "trigger" port is active-low + +// allocation of analogue pins which are not dependent on the display type that is in use +// ************************************************************************************** +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + +const byte delayBeforeSerialStarts = 1; // in seconds, to allow Serial window to be opened +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long nominalEnergyThreshold; +long workingEnergyThreshold_upper; +long workingEnergyThreshold_lower; + +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; + +#define POST_TRANSITION_MAX_COUNT 3 // <-- allows each transition to take effect + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +volatile int sampleI_grid; +volatile int sampleI_diverted; +volatile int sampleV; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define PERSISTENCE_FOR_POLARITY_CHANGE 1 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "RawSamplesTool_2chan.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + +// for this go-faster sketch, the phaseCal logic has been removed. If required, it can be +// found in most of the standard Mk2_bothDisplay_n versions + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define UPDATE_PERIOD_FOR_DISPLAYED_DATA 50 // mains cycles +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +// The two versions of the hardware require different logic. +#ifdef PIN_SAVING_HARDWARE + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. In this version, +// the decimal point has to be treated differently than the other seven segments, so +// a convenient means of accessing this column is provided. +// +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + +#else // PIN_SAVING_HARDWARE + +#define ON HIGH +#define OFF LOW + +const byte noOfSegmentsPerDigit = 8; // includes one for the decimal point +enum digitEnableStates {DIGIT_ENABLED, DIGIT_DISABLED}; + +byte digitSelectorPin[noOfDigitLocations] = {16,10,13,11}; +byte segmentDrivePin[noOfSegmentsPerDigit] = {2,5,12,6,7,9,8,14}; + +// The final column of segMap[] is for the decimal point status. In this version, +// the decimal point is treated just like all the other segments, so there is +// no need to access this column specifically. +// +byte segMap[noOfPossibleCharacters][noOfSegmentsPerDigit] = { + ON , ON , ON , ON , ON , ON , OFF, OFF, // '0' <- element 0 + OFF, ON , ON , OFF, OFF, OFF, OFF, OFF, // '1' <- element 1 + ON , ON , OFF, ON , ON , OFF, ON , OFF, // '2' <- element 2 + ON , ON , ON , ON , OFF, OFF, ON , OFF, // '3' <- element 3 + OFF, ON , ON , OFF, OFF, ON , ON , OFF, // '4' <- element 4 + ON , OFF, ON , ON , OFF, ON , ON , OFF, // '5' <- element 5 + ON , OFF, ON , ON , ON , ON , ON , OFF, // '6' <- element 6 + ON , ON , ON , OFF, OFF, OFF, OFF, OFF, // '7' <- element 7 + ON , ON , ON , ON , ON , ON , ON , OFF, // '8' <- element 8 + ON , ON , ON , ON , OFF, ON , ON , OFF, // '9' <- element 9 + ON , ON , ON , ON , ON , ON , OFF, ON , // '0.' <- element 10 + OFF, ON , ON , OFF, OFF, OFF, OFF, ON , // '1.' <- element 11 + ON , ON , OFF, ON , ON , OFF, ON , ON , // '2.' <- element 12 + ON , ON , ON , ON , OFF, OFF, ON , ON , // '3.' <- element 13 + OFF, ON , ON , OFF, OFF, ON , ON , ON , // '4.' <- element 14 + ON , OFF, ON , ON , OFF, ON , ON , ON , // '5.' <- element 15 + ON , OFF, ON , ON , ON , ON , ON , ON , // '6.' <- element 16 + ON , ON , ON , OFF, OFF, OFF, OFF, ON , // '7.' <- element 17 + ON , ON , ON , ON , ON , ON , ON , ON , // '8.' <- element 18 + ON , ON , ON , ON , OFF, ON , ON , ON , // '9.' <- element 19 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, // ' ' <- element 20 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, ON // '.' <- element 11 +}; +#endif // PIN_SAVING_HARDWARE + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // energy divertion detection +long requiredExportPerMainsCycle_inIEU; + + +void setup() +{ +// pinMode(outOfRangeIndication, OUTPUT); +// digitalWrite (outOfRangeIndication, LED_OFF); + + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for the primary load + pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for an additional load + // + for(int i = 0; i< noOfDumploads; i++) // re-using the logic from my multiLoad code + { + logicalLoadState[i] = LOAD_OFF; + physicalLoadState[i] = LOAD_OFF; + } + // + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // the primary load is active low. + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // the additional load is active high. + + delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_fasterControl_twoLoads_3.ino"); + Serial.println(); + +#ifdef PIN_SAVING_HARDWARE + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } +#else + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + pinMode(segmentDrivePin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + pinMode(digitSelectorPin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + digitalWrite(digitSelectorPin[i], DIGIT_DISABLED); } + + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + digitalWrite(segmentDrivePin[i], OFF); } +#endif + + + + // When using integer maths, calibration values that have supplied in floating point + // form need to be rescaled. + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + energyInBucket_long = 0; + + nominalEnergyThreshold = capacityOfEnergyBucket_long * 0.5; + workingEnergyThreshold_upper = nominalEnergyThreshold; // initial value + workingEnergyThreshold_lower = nominalEnergyThreshold; // initial value + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + + Serial.println ("----"); +} + +// An Interrupt Service Routine is now defined in which the ADC is instructed to +// measure each analogue input in sequence. A "data ready" flag is set after each +// voltage conversion has been completed. +// For each set of samples, the two samples for current are taken before the one +// for voltage. This is appropriate because each waveform current is generally slightly +// advanced relative to the waveform for voltage. The data ready flag is cleared +// within loop(). +// This Interrupt Service Routine is for use when the ADC is fixed timer mode. It is +// executed whenever the ADC timer expires. In this mode, the next ADC conversion is +// initiated from within this ISR. +// +void timerIsr(void) +{ + static unsigned char sample_index = 0; + static int sampleI_grid_raw; + static int sampleI_diverted_raw; + + + switch(sample_index) + { + case 0: + sampleV = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current + ADCSRA |= (1< 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + if (beyondStartUpPhase) + { + // a simple routine for checking the performance of this new ISR structure + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / sampleSetsDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step would normally be to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient to just + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + bool endOfRangeEncountered = false; + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; + endOfRangeEncountered = true;} + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; + endOfRangeEncountered = true;} + +/* + if (endOfRangeEncountered) { + digitalWrite (outOfRangeIndication , LED_ON); } + else { + digitalWrite (outOfRangeIndication , LED_OFF); } +*/ + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. + + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) + { + realEnergy_diverted = 0; + } + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + if(timerForDisplayUpdate > UPDATE_PERIOD_FOR_DISPLAYED_DATA) + { // the 4-digit display needs to be refreshed every few mS. For convenience, + // this action is performed every N times around this processing loop. + timerForDisplayUpdate = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); + } + else + { + timerForDisplayUpdate++; + } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringNegativeHalfOfMainsCycle = 0; + + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringThisMainsCycle = 0; // not yet dealt with for this cycle + sampleCount_forContinuityChecker = 1; // opportunity has been missed for this cycle + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + if(sampleSetsDuringThisMainsCycle == 5) // to distribute the workload within each mains cycle + { + if (beyondStartUpPhase) + { + // Restrictions apply for the period immediately after a load has been switched. + // Here the recentTransition flag is checked and updated as necessary. + if (recentTransition) + { + postTransitionCount++; + if (postTransitionCount >= POST_TRANSITION_MAX_COUNT) + { + recentTransition = false; + } + } + + if (recentTransition) + { + // During the post-transition period, any change in the energy level is noted. + if (energyInBucket_long > workingEnergyThreshold_upper) + { + workingEnergyThreshold_lower = nominalEnergyThreshold; // reset the opposite threshold + workingEnergyThreshold_upper = energyInBucket_long; + + // the energy thresholds must remain within range + if (workingEnergyThreshold_upper > capacityOfEnergyBucket_long) + { + workingEnergyThreshold_upper = capacityOfEnergyBucket_long; + } + } + else + if (energyInBucket_long < workingEnergyThreshold_lower) + { + workingEnergyThreshold_upper = nominalEnergyThreshold; // reset the opposite threshold + workingEnergyThreshold_lower = energyInBucket_long; + + // the energy thresholds must remain within range + if (workingEnergyThreshold_lower < 0) + { + workingEnergyThreshold_lower = 0; + } + } + } + } + } + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12); + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + // The average power that has been measured during the first half of this mains cycle can now be used + // to predict the energy state at the end of this mains cycle. That prediction will be used to alter + // the state of the load as necessary. The arming signal for the triac can't be set yet - that must + // wait until the voltage has advanced further beyond the -ve going zero-crossing point. + // + long averagePower = sumP_grid / sampleSetsDuringThisMainsCycle;// for 1st half of this mains cycle only + // + // To avoid repetitive and unnecessary calculations, the increase in energy during each mains cycle is + // deemed to be numerically equal to the average power. The predicted value for the energy state at the + // end of this mains cycle will therefore be the known energy state at its start plus the average power + // as measured. Although the average power has been determined over only half a mains cycle, the correct + // number of contributing sample sets has been used so the result can be expected to be a true measurement + // of average power, not half of it. + energyInBucket_prediction = energyInBucket_long + averagePower; // at end of this mains cycle + + + } // end of processing that is specific to the first Vsample in each -ve half cycle + + if (sampleSetsDuringNegativeHalfOfMainsCycle == 3) + { + // the zero-crossing trigger device(s) can now be reliably armed + if (beyondStartUpPhase) + { + /* Determining whether any of the loads need to be changed is is a 3-stage process: + * - change the LOGICAL load states as necessary to maintain the energy level + * - update the PHYSICAL load states according to the logical -> physical mapping + * - update the driver lines for each of the loads. + */ + + if (energyInBucket_prediction > workingEnergyThreshold_upper) + { + // the predicted energy state is high so an extra load may need to be added + boolean OK_toAddLoad = true; // default state of flag + + byte tempLoad = nextLogicalLoadToBeAdded(); + if (tempLoad < noOfDumploads) + { + // a load which is now OFF has been identified for potentially being switched ON + if (recentTransition) + { + if (tempLoad != activeLoad) + { + OK_toAddLoad = false; + } + } + + if (OK_toAddLoad) + { + // tempLoad will be either the active load, or the next load in the priority sequence + logicalLoadState[tempLoad] = LOAD_ON; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + } + } + } + else + if (energyInBucket_prediction < workingEnergyThreshold_lower) + { + // the predicted energy state is low so some load may need to be removed + boolean OK_toRemoveLoad = true; // default state of flag + + byte tempLoad = nextLogicalLoadToBeRemoved(); + if (tempLoad < noOfDumploads) + { + // a load which is now ON has been identified for potentially being switched OFF + if (recentTransition) + { + if (tempLoad != activeLoad) + { + OK_toRemoveLoad = false; + } + } + + if (OK_toRemoveLoad) + { + // tempLoad will be either the active load, or the next load in the priority sequence + logicalLoadState[tempLoad] = LOAD_OFF; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + } + } + } + + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + // update each of the physical loads + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // active high for additional load + + // update the Energy Diversion Detector + if (physicalLoadState[0] == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + } + } + + sampleSetsDuringNegativeHalfOfMainsCycle++; + } // end of processing that is specific to samples where the voltage is negative + + // processing for EVERY set of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + // + // extra filtering to offset the HPF effect of CT1 + long last_lpf_long = lpf_long; + lpf_long = last_lpf_long + alpha *(sampleIminusDC_grid - last_lpf_long); + sampleIminusDC_grid += (lpf_gain * lpf_long); + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + sampleSetsDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count > PERSISTENCE_FOR_POLARITY_CHANGE) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + +byte nextLogicalLoadToBeAdded() +{ + byte retVal = noOfDumploads; + boolean success = false; + for (byte index = 0; index < noOfDumploads && !success; index++) + { + if (logicalLoadState[index] == LOAD_OFF) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +byte nextLogicalLoadToBeRemoved() +{ + byte retVal = noOfDumploads; + boolean success = false; + + // this index counter can't be a 'byte' because the loop would run forever! + // + for (int index = (noOfDumploads -1); index >= 0 && !success; index--) + { + if (logicalLoadState[index] == LOAD_ON) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * The association between the physical and logical loads is 1:1. By default, numerical + * equivalence is maintained, so logical(N) maps to physical(N). If physical load 1 is set + * to have priority, rather than physical load 0, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * Any other mapping relaionships could be configured here. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } +} + + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + +// Serial.println(divertedEnergyTotal_Wh); + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +} + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + // The two versions of the hardware require different logic. + +#ifdef PIN_SAVING_HARDWARE + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } + +#else // PIN_SAVING_HARDWARE + + // This version is more straightforward because the digit-enable lines can be + // used to mask out all of the transitory states, including the Decimal Point. + // The sequence is: + // + // 1. de-activate the digit-enable line that was previously active + // 2. determine the next location which is to be active + // 3. determine the relevant character for the new active location + // 4. set up the segment drivers for the character to be displayed (includes the DP) + // 5. activate the digit-enable line for the new active location + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + displayTime_count = 0; + + // 1. de-activate the location which is currently being displayed + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_DISABLED); + + // 2. determine the next digit location which is to be displayed + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 3. determine the relevant character for the new active location + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 4. set up the segment drivers for the character to be displayed (includes the DP) + for (byte segment = 0; segment < noOfSegmentsPerDigit; segment++) { + byte segmentState = segMap[digitVal][segment]; + digitalWrite( segmentDrivePin[segment], segmentState); } + + // 5. activate the digit-enable line for the new active location + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_ENABLED); + } +#endif // PIN_SAVING_HARDWARE + +} // end of refreshDisplay() + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_twoLoads_4.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_twoLoads_4.ino new file mode 100644 index 0000000..5c2f5c4 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_twoLoads_4.ino @@ -0,0 +1,1194 @@ + +/* Mk2_fasterControl_twoLoads_4.ino + * + * (initially released as Mk2_bothDisplays_1 in March 2014) + * This sketch is for diverting suplus PV power to a dump load using a triac or + * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on + * the OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * In this latest version, the pin-allocations have been changed to suit my + * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is + * fed from one of the secondary coils of the transformer. Current is measured + * via Current Transformers at the CT1 and CT1 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. This can be driven in two + * different ways, one with an extra pair of logic chips, and one without. The + * appropriate version of the sketch must be selected by including or commenting + * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code. + * + * September 2014: renamed as Mk2_bothDisplays_2, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - removal of unhelpful comments in the IO pin section; + * - tidier initialisation of display logic in setup(); + * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility); + * + * December 2014: renamed as Mk2_bothDisplays_3, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed) + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances + * + * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed. + * + * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to + * remove a timing uncertainty. + * + * January 2016: updated to Mk2_bothDisplays_3c: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * February 2016: updated to Mk2_bothDisplays_4, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * November 2019: updated to Mk2_fasterControl_1 with these changes: + * - Half way through each mains cycle, a prediction is made of the likely energy level at the + * end of the cycle. That predicted value allows the triac to be switched at the +ve going + * zero-crossing point rather than waiting for a further 10 ms. These changes allow for + * faster switching of the load. + * - The range of the energy bucket has been reduced to one tenth of its former value. This + * allows the unit's operation to commence more rapidly whenever surplus power is available. + * - controlMode is no longer selectable, the unit's operation being effectively hard-coded + * as "Normal" rather than Anti-flicker. + * - Port D3 now supports an indicator which shows when the level in the energy bucket + * reaches either end of its range. While the unit is actively diverting surplus power, + * it is vital that the level in the reduced capacity energy bucket remains within its + * permitted range, hence the addition of this indicator. + * + * February 2020: updated to Mk2_fasterControl_twoLoads_1 with these changes: + * - the energy overflow indicator has been disabled to free up port D3 + * - port D3 now supports a second load + * + * February 2020: updated to Mk2_fasterControl_twoLoads_2 with these changes: + * - improved multi-load control logic to prevent the primary load from being disturbed by + * the lower priority one. This logic now mirrors that in the Mk2_multiLoad_wired_n line. + * + * March 2021: updated to Mk2_fasterControl_twoLoads_3 with these changes: + * - extra filtering added to offset the HPF effect of CT1. This allows the energy state in + * 10 ms time to be predicted with more confidence. Specifically, it is no longer necessary + * to include a 30% boost factor after each change of load state. + * + * June 2021: updated to Mk2_fasterControl_twoLoads_4 with these changes: + * - to reflect the performance of recently manufactured YHDC SCT_013_000 CTs, + * the value of the parameter lpf_gain has been reduced from 12 to 8. + * + * + * * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define WORKING_RANGE_IN_JOULES 360 // 0.1 Wh, reduced for faster start-up +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +// The two versions of the hardware require different logic. The following line should +// be included if the additional logic chips are present, or excluded if they are +// absent (in which case some wire links need to be fitted) +// +//#define PIN_SAVING_HARDWARE + +const byte noOfDumploads = 2; + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are logically active-low +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +// For this go-faster version, the unit's operation will effectively always be "Normal"; +// there is no "Anti-flicker" option. The controlMode variable has been removed. + +// allocation of digital pins which are not dependent on the display type that is in use +// ************************************************************************************* +// const byte outOfRangeIndication = 3; // <-- this output port is active-high +const byte physicalLoad_1_pin = 3; // <-- the "mode" port is active-high +const byte physicalLoad_0_pin = 4; // <-- the "trigger" port is active-low + +// allocation of analogue pins which are not dependent on the display type that is in use +// ************************************************************************************** +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + +const byte delayBeforeSerialStarts = 1; // in seconds, to allow Serial window to be opened +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long nominalEnergyThreshold; +long workingEnergyThreshold_upper; +long workingEnergyThreshold_lower; + +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; + +#define POST_TRANSITION_MAX_COUNT 3 // <-- allows each transition to take effect + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +volatile int sampleI_grid; +volatile int sampleI_diverted; +volatile int sampleV; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define PERSISTENCE_FOR_POLARITY_CHANGE 1 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "RawSamplesTool_2chan.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + +// for this go-faster sketch, the phaseCal logic has been removed. If required, it can be +// found in most of the standard Mk2_bothDisplay_n versions + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define UPDATE_PERIOD_FOR_DISPLAYED_DATA 50 // mains cycles +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +// The two versions of the hardware require different logic. +#ifdef PIN_SAVING_HARDWARE + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. In this version, +// the decimal point has to be treated differently than the other seven segments, so +// a convenient means of accessing this column is provided. +// +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + +#else // PIN_SAVING_HARDWARE + +#define ON HIGH +#define OFF LOW + +const byte noOfSegmentsPerDigit = 8; // includes one for the decimal point +enum digitEnableStates {DIGIT_ENABLED, DIGIT_DISABLED}; + +byte digitSelectorPin[noOfDigitLocations] = {16,10,13,11}; +byte segmentDrivePin[noOfSegmentsPerDigit] = {2,5,12,6,7,9,8,14}; + +// The final column of segMap[] is for the decimal point status. In this version, +// the decimal point is treated just like all the other segments, so there is +// no need to access this column specifically. +// +byte segMap[noOfPossibleCharacters][noOfSegmentsPerDigit] = { + ON , ON , ON , ON , ON , ON , OFF, OFF, // '0' <- element 0 + OFF, ON , ON , OFF, OFF, OFF, OFF, OFF, // '1' <- element 1 + ON , ON , OFF, ON , ON , OFF, ON , OFF, // '2' <- element 2 + ON , ON , ON , ON , OFF, OFF, ON , OFF, // '3' <- element 3 + OFF, ON , ON , OFF, OFF, ON , ON , OFF, // '4' <- element 4 + ON , OFF, ON , ON , OFF, ON , ON , OFF, // '5' <- element 5 + ON , OFF, ON , ON , ON , ON , ON , OFF, // '6' <- element 6 + ON , ON , ON , OFF, OFF, OFF, OFF, OFF, // '7' <- element 7 + ON , ON , ON , ON , ON , ON , ON , OFF, // '8' <- element 8 + ON , ON , ON , ON , OFF, ON , ON , OFF, // '9' <- element 9 + ON , ON , ON , ON , ON , ON , OFF, ON , // '0.' <- element 10 + OFF, ON , ON , OFF, OFF, OFF, OFF, ON , // '1.' <- element 11 + ON , ON , OFF, ON , ON , OFF, ON , ON , // '2.' <- element 12 + ON , ON , ON , ON , OFF, OFF, ON , ON , // '3.' <- element 13 + OFF, ON , ON , OFF, OFF, ON , ON , ON , // '4.' <- element 14 + ON , OFF, ON , ON , OFF, ON , ON , ON , // '5.' <- element 15 + ON , OFF, ON , ON , ON , ON , ON , ON , // '6.' <- element 16 + ON , ON , ON , OFF, OFF, OFF, OFF, ON , // '7.' <- element 17 + ON , ON , ON , ON , ON , ON , ON , ON , // '8.' <- element 18 + ON , ON , ON , ON , OFF, ON , ON , ON , // '9.' <- element 19 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, // ' ' <- element 20 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, ON // '.' <- element 11 +}; +#endif // PIN_SAVING_HARDWARE + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // energy divertion detection +long requiredExportPerMainsCycle_inIEU; + + +void setup() +{ +// pinMode(outOfRangeIndication, OUTPUT); +// digitalWrite (outOfRangeIndication, LED_OFF); + + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for the primary load + pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for an additional load + // + for(int i = 0; i< noOfDumploads; i++) // re-using the logic from my multiLoad code + { + logicalLoadState[i] = LOAD_OFF; + physicalLoadState[i] = LOAD_OFF; + } + // + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // the primary load is active low. + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // the additional load is active high. + + delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_fasterControl_twoLoads_4.ino"); + Serial.println(); + +#ifdef PIN_SAVING_HARDWARE + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } +#else + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + pinMode(segmentDrivePin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + pinMode(digitSelectorPin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + digitalWrite(digitSelectorPin[i], DIGIT_DISABLED); } + + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + digitalWrite(segmentDrivePin[i], OFF); } +#endif + + + + // When using integer maths, calibration values that have supplied in floating point + // form need to be rescaled. + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + energyInBucket_long = 0; + + nominalEnergyThreshold = capacityOfEnergyBucket_long * 0.5; + workingEnergyThreshold_upper = nominalEnergyThreshold; // initial value + workingEnergyThreshold_lower = nominalEnergyThreshold; // initial value + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + + Serial.println ("----"); +} + +// An Interrupt Service Routine is now defined in which the ADC is instructed to +// measure each analogue input in sequence. A "data ready" flag is set after each +// voltage conversion has been completed. +// For each set of samples, the two samples for current are taken before the one +// for voltage. This is appropriate because each waveform current is generally slightly +// advanced relative to the waveform for voltage. The data ready flag is cleared +// within loop(). +// This Interrupt Service Routine is for use when the ADC is fixed timer mode. It is +// executed whenever the ADC timer expires. In this mode, the next ADC conversion is +// initiated from within this ISR. +// +void timerIsr(void) +{ + static unsigned char sample_index = 0; + static int sampleI_grid_raw; + static int sampleI_diverted_raw; + + + switch(sample_index) + { + case 0: + sampleV = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current + ADCSRA |= (1< 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + if (beyondStartUpPhase) + { + // a simple routine for checking the performance of this new ISR structure + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / sampleSetsDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step would normally be to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient to just + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + bool endOfRangeEncountered = false; + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; + endOfRangeEncountered = true;} + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; + endOfRangeEncountered = true;} + +/* + if (endOfRangeEncountered) { + digitalWrite (outOfRangeIndication , LED_ON); } + else { + digitalWrite (outOfRangeIndication , LED_OFF); } +*/ + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. + + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) + { + realEnergy_diverted = 0; + } + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + if(timerForDisplayUpdate > UPDATE_PERIOD_FOR_DISPLAYED_DATA) + { // the 4-digit display needs to be refreshed every few mS. For convenience, + // this action is performed every N times around this processing loop. + timerForDisplayUpdate = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); + } + else + { + timerForDisplayUpdate++; + } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringNegativeHalfOfMainsCycle = 0; + + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringThisMainsCycle = 0; // not yet dealt with for this cycle + sampleCount_forContinuityChecker = 1; // opportunity has been missed for this cycle + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + if(sampleSetsDuringThisMainsCycle == 5) // to distribute the workload within each mains cycle + { + if (beyondStartUpPhase) + { + // Restrictions apply for the period immediately after a load has been switched. + // Here the recentTransition flag is checked and updated as necessary. + if (recentTransition) + { + postTransitionCount++; + if (postTransitionCount >= POST_TRANSITION_MAX_COUNT) + { + recentTransition = false; + } + } + + if (recentTransition) + { + // During the post-transition period, any change in the energy level is noted. + if (energyInBucket_long > workingEnergyThreshold_upper) + { + workingEnergyThreshold_lower = nominalEnergyThreshold; // reset the opposite threshold + workingEnergyThreshold_upper = energyInBucket_long; + + // the energy thresholds must remain within range + if (workingEnergyThreshold_upper > capacityOfEnergyBucket_long) + { + workingEnergyThreshold_upper = capacityOfEnergyBucket_long; + } + } + else + if (energyInBucket_long < workingEnergyThreshold_lower) + { + workingEnergyThreshold_upper = nominalEnergyThreshold; // reset the opposite threshold + workingEnergyThreshold_lower = energyInBucket_long; + + // the energy thresholds must remain within range + if (workingEnergyThreshold_lower < 0) + { + workingEnergyThreshold_lower = 0; + } + } + } + } + } + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12); + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + // The average power that has been measured during the first half of this mains cycle can now be used + // to predict the energy state at the end of this mains cycle. That prediction will be used to alter + // the state of the load as necessary. The arming signal for the triac can't be set yet - that must + // wait until the voltage has advanced further beyond the -ve going zero-crossing point. + // + long averagePower = sumP_grid / sampleSetsDuringThisMainsCycle;// for 1st half of this mains cycle only + // + // To avoid repetitive and unnecessary calculations, the increase in energy during each mains cycle is + // deemed to be numerically equal to the average power. The predicted value for the energy state at the + // end of this mains cycle will therefore be the known energy state at its start plus the average power + // as measured. Although the average power has been determined over only half a mains cycle, the correct + // number of contributing sample sets has been used so the result can be expected to be a true measurement + // of average power, not half of it. + energyInBucket_prediction = energyInBucket_long + averagePower; // at end of this mains cycle + + + } // end of processing that is specific to the first Vsample in each -ve half cycle + + if (sampleSetsDuringNegativeHalfOfMainsCycle == 3) + { + // the zero-crossing trigger device(s) can now be reliably armed + if (beyondStartUpPhase) + { + /* Determining whether any of the loads need to be changed is is a 3-stage process: + * - change the LOGICAL load states as necessary to maintain the energy level + * - update the PHYSICAL load states according to the logical -> physical mapping + * - update the driver lines for each of the loads. + */ + + if (energyInBucket_prediction > workingEnergyThreshold_upper) + { + // the predicted energy state is high so an extra load may need to be added + boolean OK_toAddLoad = true; // default state of flag + + byte tempLoad = nextLogicalLoadToBeAdded(); + if (tempLoad < noOfDumploads) + { + // a load which is now OFF has been identified for potentially being switched ON + if (recentTransition) + { + if (tempLoad != activeLoad) + { + OK_toAddLoad = false; + } + } + + if (OK_toAddLoad) + { + // tempLoad will be either the active load, or the next load in the priority sequence + logicalLoadState[tempLoad] = LOAD_ON; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + } + } + } + else + if (energyInBucket_prediction < workingEnergyThreshold_lower) + { + // the predicted energy state is low so some load may need to be removed + boolean OK_toRemoveLoad = true; // default state of flag + + byte tempLoad = nextLogicalLoadToBeRemoved(); + if (tempLoad < noOfDumploads) + { + // a load which is now ON has been identified for potentially being switched OFF + if (recentTransition) + { + if (tempLoad != activeLoad) + { + OK_toRemoveLoad = false; + } + } + + if (OK_toRemoveLoad) + { + // tempLoad will be either the active load, or the next load in the priority sequence + logicalLoadState[tempLoad] = LOAD_OFF; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + } + } + } + + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + // update each of the physical loads + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // active high for additional load + + // update the Energy Diversion Detector + if (physicalLoadState[0] == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + } + } + + sampleSetsDuringNegativeHalfOfMainsCycle++; + } // end of processing that is specific to samples where the voltage is negative + + // processing for EVERY set of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + // + // extra filtering to offset the HPF effect of CT1 + long last_lpf_long = lpf_long; + lpf_long = last_lpf_long + alpha *(sampleIminusDC_grid - last_lpf_long); + sampleIminusDC_grid += (lpf_gain * lpf_long); + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + sampleSetsDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count > PERSISTENCE_FOR_POLARITY_CHANGE) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + +byte nextLogicalLoadToBeAdded() +{ + byte retVal = noOfDumploads; + boolean success = false; + for (byte index = 0; index < noOfDumploads && !success; index++) + { + if (logicalLoadState[index] == LOAD_OFF) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +byte nextLogicalLoadToBeRemoved() +{ + byte retVal = noOfDumploads; + boolean success = false; + + // this index counter can't be a 'byte' because the loop would run forever! + // + for (int index = (noOfDumploads -1); index >= 0 && !success; index--) + { + if (logicalLoadState[index] == LOAD_ON) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * The association between the physical and logical loads is 1:1. By default, numerical + * equivalence is maintained, so logical(N) maps to physical(N). If physical load 1 is set + * to have priority, rather than physical load 0, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * Any other mapping relaionships could be configured here. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } +} + + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + +// Serial.println(divertedEnergyTotal_Wh); + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +} + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + // The two versions of the hardware require different logic. + +#ifdef PIN_SAVING_HARDWARE + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } + +#else // PIN_SAVING_HARDWARE + + // This version is more straightforward because the digit-enable lines can be + // used to mask out all of the transitory states, including the Decimal Point. + // The sequence is: + // + // 1. de-activate the digit-enable line that was previously active + // 2. determine the next location which is to be active + // 3. determine the relevant character for the new active location + // 4. set up the segment drivers for the character to be displayed (includes the DP) + // 5. activate the digit-enable line for the new active location + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + displayTime_count = 0; + + // 1. de-activate the location which is currently being displayed + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_DISABLED); + + // 2. determine the next digit location which is to be displayed + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 3. determine the relevant character for the new active location + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 4. set up the segment drivers for the character to be displayed (includes the DP) + for (byte segment = 0; segment < noOfSegmentsPerDigit; segment++) { + byte segmentState = segMap[digitVal][segment]; + digitalWrite( segmentDrivePin[segment], segmentState); } + + // 5. activate the digit-enable line for the new active location + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_ENABLED); + } +#endif // PIN_SAVING_HARDWARE + +} // end of refreshDisplay() + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_twoLoads_5.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_twoLoads_5.ino new file mode 100644 index 0000000..491401a --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_twoLoads_5.ino @@ -0,0 +1,1201 @@ +/* Mk2_fasterControl_twoLoads_5.ino + * + * (initially released as Mk2_bothDisplays_1 in March 2014) + * This sketch is for diverting suplus PV power to a dump load using a triac or + * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on + * the OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * In this latest version, the pin-allocations have been changed to suit my + * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is + * fed from one of the secondary coils of the transformer. Current is measured + * via Current Transformers at the CT1 and CT1 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. This can be driven in two + * different ways, one with an extra pair of logic chips, and one without. The + * appropriate version of the sketch must be selected by including or commenting + * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code. + * + * September 2014: renamed as Mk2_bothDisplays_2, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - removal of unhelpful comments in the IO pin section; + * - tidier initialisation of display logic in setup(); + * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility); + * + * December 2014: renamed as Mk2_bothDisplays_3, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed) + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances + * + * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed. + * + * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to + * remove a timing uncertainty. + * + * January 2016: updated to Mk2_bothDisplays_3c: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * February 2016: updated to Mk2_bothDisplays_4, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * November 2019: updated to Mk2_fasterControl_1 with these changes: + * - Half way through each mains cycle, a prediction is made of the likely energy level at the + * end of the cycle. That predicted value allows the triac to be switched at the +ve going + * zero-crossing point rather than waiting for a further 10 ms. These changes allow for + * faster switching of the load. + * - The range of the energy bucket has been reduced to one tenth of its former value. This + * allows the unit's operation to commence more rapidly whenever surplus power is available. + * - controlMode is no longer selectable, the unit's operation being effectively hard-coded + * as "Normal" rather than Anti-flicker. + * - Port D3 now supports an indicator which shows when the level in the energy bucket + * reaches either end of its range. While the unit is actively diverting surplus power, + * it is vital that the level in the reduced capacity energy bucket remains within its + * permitted range, hence the addition of this indicator. + * + * February 2020: updated to Mk2_fasterControl_1a with these changes: + * - removal of some redundant code in the logic for determining the next load state. + * + * March 2021: updated to Mk2_fasterControl_2 with these changes: + * - extra filtering added to offset the HPF effect of CT1. This allows the energy state in + * 10 ms time to be predicted with more confidence. Specifically, it is no longer necessary + * to include a 30% boost factor after each change of load state. + * + * June 2021: updated to Mk2_fasterControl_3 with these changes: + * - to reflect the performance of recently manufactured YHDC SCT_013_000 CTs, + * the value of the parameter lpf_gain has been reduced from 12 to 8. + * + * July 2023: updated to Mk2_fasterControl_twoLoads_5 with these changes: + * - the ability to control two loads has been transferred from the latest version of my + * standard multiLoad sketch, Mk2_multiLoad_wired_7a. The faster control algorithm + * has been retained. + * The previous 2-load "faster control" sketch (version 4) has been archived as its + * behaviour was found to be problematic. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define WORKING_RANGE_IN_JOULES 360 // 0.1 Wh, reduced for faster start-up +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +const byte noOfDumploads = 2; // The logic expects a minimum of 2 dumploads, + // for local & remote loads, but neither has to + // be physically present. + +// The two versions of the hardware require different logic. The following line should +// be included if the additional logic chips are present, or excluded if they are +// absent (in which case some wire links need to be fitted) +// +#define PIN_SAVING_HARDWARE + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are active low +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +// For this go-faster version, the unit's operation will effectively always be "Normal"; +// there is no "Anti-flicker" option. The controlMode variable has been removed. + +// allocation of digital pins which are not dependent on the display type that is in use +// ************************************************************************************* +const byte physicalLoad_1_pin = 3; // <-- the "mode" port is active-high +const byte physicalLoad_0_pin = 4; // <-- the "trigger" port is active-low + +// allocation of analogue pins which are not dependent on the display type that is in use +// ************************************************************************************** +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + +const byte delayBeforeSerialStarts = 1; // in seconds, to allow Serial window to be opened +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long midPointOfEnergyBucket_long; // used for 'normal' and single-threshold 'AF' logic + +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; +long mainsCyclesPerHour; + +long lowerThreshold_default; +long lowerEnergyThreshold; +long upperThreshold_default; +long upperEnergyThreshold; + +boolean recentTransition = false; +byte postTransitionCount; +#define POST_TRANSITION_MAX_COUNT 3 // <-- allows each transition to take effect +byte activeLoad = 0; + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +volatile int sampleI_grid; +volatile int sampleI_diverted; +volatile int sampleV; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define PERSISTENCE_FOR_POLARITY_CHANGE 1 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "RawSamplesTool_2chan.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + +// for this go-faster sketch, the phaseCal logic has been removed. If required, it can be +// found in most of the standard Mk2_bothDisplay_n versions + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define UPDATE_PERIOD_FOR_DISPLAYED_DATA 50 // mains cycles +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +// The two versions of the hardware require different logic. +#ifdef PIN_SAVING_HARDWARE + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. In this version, +// the decimal point has to be treated differently than the other seven segments, so +// a convenient means of accessing this column is provided. +// +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + +#else // PIN_SAVING_HARDWARE + +#define ON HIGH +#define OFF LOW + +const byte noOfSegmentsPerDigit = 8; // includes one for the decimal point +enum digitEnableStates {DIGIT_ENABLED, DIGIT_DISABLED}; + +byte digitSelectorPin[noOfDigitLocations] = {16,10,13,11}; +byte segmentDrivePin[noOfSegmentsPerDigit] = {2,5,12,6,7,9,8,14}; + +// The final column of segMap[] is for the decimal point status. In this version, +// the decimal point is treated just like all the other segments, so there is +// no need to access this column specifically. +// +byte segMap[noOfPossibleCharacters][noOfSegmentsPerDigit] = { + ON , ON , ON , ON , ON , ON , OFF, OFF, // '0' <- element 0 + OFF, ON , ON , OFF, OFF, OFF, OFF, OFF, // '1' <- element 1 + ON , ON , OFF, ON , ON , OFF, ON , OFF, // '2' <- element 2 + ON , ON , ON , ON , OFF, OFF, ON , OFF, // '3' <- element 3 + OFF, ON , ON , OFF, OFF, ON , ON , OFF, // '4' <- element 4 + ON , OFF, ON , ON , OFF, ON , ON , OFF, // '5' <- element 5 + ON , OFF, ON , ON , ON , ON , ON , OFF, // '6' <- element 6 + ON , ON , ON , OFF, OFF, OFF, OFF, OFF, // '7' <- element 7 + ON , ON , ON , ON , ON , ON , ON , OFF, // '8' <- element 8 + ON , ON , ON , ON , OFF, ON , ON , OFF, // '9' <- element 9 + ON , ON , ON , ON , ON , ON , OFF, ON , // '0.' <- element 10 + OFF, ON , ON , OFF, OFF, OFF, OFF, ON , // '1.' <- element 11 + ON , ON , OFF, ON , ON , OFF, ON , ON , // '2.' <- element 12 + ON , ON , ON , ON , OFF, OFF, ON , ON , // '3.' <- element 13 + OFF, ON , ON , OFF, OFF, ON , ON , ON , // '4.' <- element 14 + ON , OFF, ON , ON , OFF, ON , ON , ON , // '5.' <- element 15 + ON , OFF, ON , ON , ON , ON , ON , ON , // '6.' <- element 16 + ON , ON , ON , OFF, OFF, OFF, OFF, ON , // '7.' <- element 17 + ON , ON , ON , ON , ON , ON , ON , ON , // '8.' <- element 18 + ON , ON , ON , ON , OFF, ON , ON , ON , // '9.' <- element 19 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, // ' ' <- element 20 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, ON // '.' <- element 11 +}; +#endif // PIN_SAVING_HARDWARE + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // energy divertion detection +long requiredExportPerMainsCycle_inIEU; + + +void setup() +{ + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for the local dump-load + pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for an additional load + + for(int i = 0; i< noOfDumploads; i++) + { + logicalLoadState[i] = LOAD_OFF; + physicalLoadState[i] = LOAD_OFF; + } + + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // the local load is active low. + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // additional loads are active high. + + delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_fasterControl_twoLoads_5.ino"); + Serial.println(); + +#ifdef PIN_SAVING_HARDWARE + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } +#else + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + pinMode(segmentDrivePin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + pinMode(digitSelectorPin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + digitalWrite(digitSelectorPin[i], DIGIT_DISABLED); } + + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + digitalWrite(segmentDrivePin[i], OFF); } +#endif + + + + // When using integer maths, calibration values that have supplied in floating point + // form need to be rescaled. + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + midPointOfEnergyBucket_long = capacityOfEnergyBucket_long / 2; + lowerThreshold_default = capacityOfEnergyBucket_long * 0.5; + upperThreshold_default = capacityOfEnergyBucket_long * 0.5; + energyInBucket_long = 0; + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + + Serial.println ("----"); +} + +// An Interrupt Service Routine is now defined in which the ADC is instructed to +// measure each analogue input in sequence. A "data ready" flag is set after each +// voltage conversion has been completed. +// For each set of samples, the two samples for current are taken before the one +// for voltage. This is appropriate because each waveform current is generally slightly +// advanced relative to the waveform for voltage. The data ready flag is cleared +// within loop(). +// This Interrupt Service Routine is for use when the ADC is fixed timer mode. It is +// executed whenever the ADC timer expires. In this mode, the next ADC conversion is +// initiated from within this ISR. +// +void timerIsr(void) +{ + static unsigned char sample_index = 0; + static int sampleI_grid_raw; + static int sampleI_diverted_raw; + + + switch(sample_index) + { + case 0: + sampleV = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current + ADCSRA |= (1< 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + if (beyondStartUpPhase) + { + // a simple routine for checking the performance of this new ISR structure + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / sampleSetsDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step would normally be to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient to just + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long;} + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0;} + + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. + + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) + { + realEnergy_diverted = 0; + } + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + if(timerForDisplayUpdate > UPDATE_PERIOD_FOR_DISPLAYED_DATA) + { // the 4-digit display needs to be refreshed every few mS. For convenience, + // this action is performed every N times around this processing loop. + timerForDisplayUpdate = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); +// Serial.println(energyInBucket_prediction); + } + else + { + timerForDisplayUpdate++; + } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringNegativeHalfOfMainsCycle = 0; + + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringThisMainsCycle = 0; // not yet dealt with for this cycle + sampleCount_forContinuityChecker = 1; // opportunity has been missed for this cycle + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + // (in this go-faster code, the action from here has moved to the negative half of the cycle) + + } // end of processing that is specific to samples where the voltage is positive + + else // the polarity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12); + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + // The average power that has been measured during the first half of this mains cycle can now be used + // to predict the energy state at the end of this mains cycle. That prediction will be used to alter + // the state of the load as necessary. The arming signal for the triac can't be set yet - that must + // wait until the voltage has advanced further beyond the -ve going zero-crossing point. + // + long averagePower = sumP_grid / sampleSetsDuringThisMainsCycle;// for 1st half of this mains cycle only + // + // To avoid repetitive and unnecessary calculations, the increase in energy during each mains cycle is + // deemed to be numerically equal to the average power. The predicted value for the energy state at the + // end of this mains cycle will therefore be the known energy state at its start plus the average power + // as measured. Although the average power has been determined over only half a mains cycle, the correct + // number of contributing sample sets has been used so the result can be expected to be a true measurement + // of average power, not half of it. + // + energyInBucket_prediction = energyInBucket_long + averagePower; // at end of this mains cycle + + + } // end of processing that is specific to the first Vsample in each -ve half cycle + + // check to see whether the trigger device can now be reliably armed + if (sampleSetsDuringNegativeHalfOfMainsCycle == 3) + { + if (beyondStartUpPhase) + { + /* Determining whether any of the loads need to be changed is is a 3-stage process: + * - change the LOGICAL load states as necessary to maintain the energy level + * - update the PHYSICAL load states according to the logical -> physical mapping + * - update the driver lines for each of the loads. + */ + + // Restrictions apply for the period immediately after a load has been switched. + // Here the recentTransition flag is checked and updated as necessary. + if (recentTransition) + { + postTransitionCount++; + if (postTransitionCount >= POST_TRANSITION_MAX_COUNT) + { + recentTransition = false; + } + } + + if (energyInBucket_prediction > midPointOfEnergyBucket_long) + { + // the energy state is in the upper half of the working range + lowerEnergyThreshold = lowerThreshold_default; // reset the "opposite" threshold + if (energyInBucket_prediction > upperEnergyThreshold) + { + // Because the energy level is high, some action may be required + boolean OK_toAddLoad = true; + byte tempLoad = nextLogicalLoadToBeAdded(); + if (tempLoad < noOfDumploads) + { + // a load which is now OFF has been identified for potentially being switched ON + if (recentTransition) + { + // During the post-transition period, any increase in the energy level is noted. + upperEnergyThreshold = energyInBucket_prediction; + + // the energy thresholds must remain within range + if (upperEnergyThreshold > capacityOfEnergyBucket_long) + { + upperEnergyThreshold = capacityOfEnergyBucket_long; + } + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toAddLoad = false; + } + } + + if (OK_toAddLoad) + { + logicalLoadState[tempLoad] = LOAD_ON; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + } + } + } + } + else + { // the energy state is in the lower half of the working range + upperEnergyThreshold = upperThreshold_default; // reset the "opposite" threshold + if (energyInBucket_prediction < lowerEnergyThreshold) + { + // Because the energy level is low, some action may be required + boolean OK_toRemoveLoad = true; + byte tempLoad = nextLogicalLoadToBeRemoved(); + if (tempLoad < noOfDumploads) + { + // a load which is now ON has been identified for potentially being switched OFF + if (recentTransition) + { + // During the post-transition period, any decrease in the energy level is noted. + lowerEnergyThreshold = energyInBucket_prediction; + + // the energy thresholds must remain within range + if (lowerEnergyThreshold < 0) + { + lowerEnergyThreshold = 0; + } + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toRemoveLoad = false; + } + } + + if (OK_toRemoveLoad) + { + logicalLoadState[tempLoad] = LOAD_OFF; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + } + } + } + } + + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + // update each of the physical loads + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // active high for additional load + + // update the Energy Diversion Detector + if (physicalLoadState[0] == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + + // Now that the energy-related decisions have been taken, min and max limits can now + // be applied to the level of the energy bucket. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + } + } + + sampleSetsDuringNegativeHalfOfMainsCycle++; + } // end of processing that is specific to samples where the voltage is negative + + // processing for EVERY set of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + // + // extra filtering to offset the HPF effect of CT1 + long last_lpf_long = lpf_long; + lpf_long = last_lpf_long + alpha *(sampleIminusDC_grid - last_lpf_long); + sampleIminusDC_grid += (lpf_gain * lpf_long); + + // + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + sampleSetsDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count > PERSISTENCE_FOR_POLARITY_CHANGE) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + +byte nextLogicalLoadToBeAdded() +{ + byte retVal = noOfDumploads; + boolean success = false; + for (byte index = 0; index < noOfDumploads && !success; index++) + { + if (logicalLoadState[index] == LOAD_OFF) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +byte nextLogicalLoadToBeRemoved() +{ + byte retVal = noOfDumploads; + boolean success = false; + + // this index counter can't be a 'byte' because the loop would run forever! + // + for (int index = (noOfDumploads -1); index >= 0 && !success; index--) + { + if (logicalLoadState[index] == LOAD_ON) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * The association between the physical and logical loads is 1:1. By default, numerical + * equivalence is maintained, so logical(N) maps to physical(N). If physical load 1 is set + * to have priority, rather than physical load 0, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * Any other mapping relaionships could be configured here. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } + +/* + if (loadPriorityMode == LOAD_1_HAS_PRIORITY) + { + // swap physical loads 0 & 1 if remote load has priority + physicalLoadState[0] = logicalLoadState[1]; + physicalLoadState[1] = logicalLoadState[0]; + } +*/ +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + +// Serial.println(divertedEnergyTotal_Wh); + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +} + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + // The two versions of the hardware require different logic. + +#ifdef PIN_SAVING_HARDWARE + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } + +#else // PIN_SAVING_HARDWARE + + // This version is more straightforward because the digit-enable lines can be + // used to mask out all of the transitory states, including the Decimal Point. + // The sequence is: + // + // 1. de-activate the digit-enable line that was previously active + // 2. determine the next location which is to be active + // 3. determine the relevant character for the new active location + // 4. set up the segment drivers for the character to be displayed (includes the DP) + // 5. activate the digit-enable line for the new active location + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + displayTime_count = 0; + + // 1. de-activate the location which is currently being displayed + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_DISABLED); + + // 2. determine the next digit location which is to be displayed + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 3. determine the relevant character for the new active location + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 4. set up the segment drivers for the character to be displayed (includes the DP) + for (byte segment = 0; segment < noOfSegmentsPerDigit; segment++) { + byte segmentState = segMap[digitVal][segment]; + digitalWrite( segmentDrivePin[segment], segmentState); } + + // 5. activate the digit-enable line for the new active location + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_ENABLED); + } +#endif // PIN_SAVING_HARDWARE + +} // end of refreshDisplay() + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_withRemoteLoad_1.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_withRemoteLoad_1.ino new file mode 100644 index 0000000..3eeb7ef --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_fasterControl_withRemoteLoad_1.ino @@ -0,0 +1,1158 @@ +/* Mk2_fasterControl_withRemoteLoad_1.ino + * + * (initially released as Mk2_bothDisplays_1 in March 2014) + * This sketch is for diverting suplus PV power to a dump load using a triac or + * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on + * the OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * In this latest version, the pin-allocations have been changed to suit my + * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is + * fed from one of the secondary coils of the transformer. Current is measured + * via Current Transformers at the CT1 and CT1 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. With this sketch that supports a remote load + * that is controlled via RF, the display can only be used with the pin-saving hardware: ICs 3&4. + * + * September 2014: renamed as Mk2_bothDisplays_2, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - removal of unhelpful comments in the IO pin section; + * - tidier initialisation of display logic in setup(); + * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility); + * + * December 2014: renamed as Mk2_bothDisplays_3, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed) + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances + * + * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed. + * + * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to + * remove a timing uncertainty. + * + * January 2016: updated to Mk2_bothDisplays_3c: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * February 2016: updated to Mk2_bothDisplays_4, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * November 2019: updated to Mk2_fasterControl_1 with these changes: + * - Half way through each mains cycle, a prediction is made of the likely energy level at the + * end of the cycle. That predicted value allows the triac to be switched at the +ve going + * zero-crossing point rather than waiting for a further 10 ms. These changes allow for + * faster switching of the load. + * - The range of the energy bucket has been reduced to one tenth of its former value. This + * allows the unit's operation to commence more rapidly whenever surplus power is available. + * - controlMode is no longer selectable, the unit's operation being effectively hard-coded + * as "Normal" rather than Anti-flicker. + * - Port D3 now supports an indicator which shows when the level in the energy bucket + * reaches either end of its range. While the unit is actively diverting surplus power, + * it is vital that the level in the reduced capacity energy bucket remains within its + * permitted range, hence the addition of this indicator. + * + * February 2020: updated to Mk2_fasterControl_twoLoads_1 with these changes: + * - the energy overflow indicator has been disabled to free up port D3 + * - port D3 now supports a second load + * + * February 2020: updated to Mk2_fasterControl_twoLoads_2 with these changes: + * - improved multi-load control logic to prevent the primary load from being disturbed by + * the lower priority one. This logic now mirrors that in the Mk2_multiLoad_wired_n line. + * + * March 2021: updated to Mk2_fasterControl_withRF_1 with these changes: + * - addition of datalogging by RF + * - removal of the option for standard display hardware (which is incompatible with RF) + * + * March 2021: updated to Mk2_fasterControl_2 with these changes: + * - extra filtering added to offset the HPF effect of CT1. This allows the energy state in + * 10 ms time to be predicted with more confidence. Specifically, it is no longer necessary + * to include a 30% boost factor after each change of load state. + * + * June 2021: updated to Mk2_fasterControl_withRF_3 with these changes: + * - to reflect the performance of recently manufactured YHDC SCT_013_000 CTs, + * the value of the parameter lpf_gain has been reduced from 12 to 8. + * + * July 2022: updated to Mk2_fasterControl_withRF_4, with this change: + * - the datalogging accumulators for grid power, diverted power and Vsquared have been rescaled + * to 1/16 of their previous values to avoid the risk of overflowing during a 10-second + * datalogging period. + * + * September 2022: updated to Mk2_fasterControl_withRemoteLoad_1 with these changes: + * - remove all code for datalogging + * - add code to support a remote load via RF control. A one-integer on/off instruction can be sent every + * mains cycle with a refresh message being sent every 5 mains cycles if the required state of + * the load has not changed. For use with the receiver sketch, remoteUnit_fasterControl_n. + * - increase the hardware timer duriation from 125 us to 150 us (just to reduce the workload) + * + * Robin Emley + * + * www.Mk2PVrouter.co.uk + */ + +#include +#include + +#define RF_PRESENT // <- this line should be commented out if the RFM12B module is not present +// +#ifdef RF_PRESENT +#define RF69_COMPAT 0 // for the RFM12B +// #define RF69_COMPAT 1 // for the RF69 +#include +#endif + +#define ADC_TIMER_PERIOD 150 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define RF_SEND_PERIOD 10 // seconds +#define WORKING_RANGE_IN_JOULES 360 // 0.1 Wh, reduced for faster start-up +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +const byte noOfDumploads = 2; + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are logically active-low +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +// For this go-faster version, the unit's operation will effectively always be "Normal"; +// there is no "Anti-flicker" option. The controlMode variable has therefore been removed. + +/* -------------------------------------- + * RF configuration (for the RFM12B of RF69CW module) + * frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ + */ +#ifdef RF_PRESENT +#define freq RF12_433MHZ + +const int nodeID = 10; // RFM12B node ID +const int networkGroup = 210; // wireless network group - needs to be same for all nodes +const int UNO = 1; // for when the processor contains the UNO bootloader. +#endif + +typedef struct { + int dumpState; +} Tx_struct; +Tx_struct tx_data; + +// allocation of digital IO pins +// ***************************** +const byte physicalLoad_1_pin = 3; // <-- a local indication for the remote load state +const byte physicalLoad_0_pin = 4; // <-- the "trigger" port is active-low + +// allocation of analogue IO pins +// ****************************** +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + +const byte delayBeforeSerialStarts = 1; // in seconds, to allow Serial window to be opened +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long nominalEnergyThreshold; +long workingEnergyThreshold_upper; +long workingEnergyThreshold_lower; + +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh_diverted; + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; + +#define POST_TRANSITION_MAX_COUNT 3 // <-- allows each transition to take effect + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +volatile int sampleI_grid; +volatile int sampleI_diverted; +volatile int sampleV; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define PERSISTENCE_FOR_POLARITY_CHANGE 1 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and voltageCal. +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "RawSamplesTool_2chan.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-Vcc and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 230V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 230V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt. +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.062; +const float powerCal_diverted = 0.062; + +// for this go-faster sketch, the phaseCal logic has been removed. If required, it can be +// found in most of the standard Mk2_bothDisplay_n versions + + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +// This sketch can only use the pin-saving hardware (ICs 3&4). +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. In this version, +// the decimal point has to be treated differently than the other seven segments, so +// a convenient means of accessing this column is provided. +// +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // energy divertion detection +long requiredExportPerMainsCycle_inIEU; + + +void setup() +{ +// pinMode(outOfRangeIndication, OUTPUT); +// digitalWrite (outOfRangeIndication, LED_OFF); + + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for the primary load + pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for an additional load + // + for(int i = 0; i< noOfDumploads; i++) // re-using the logic from my multiLoad code + { + logicalLoadState[i] = LOAD_OFF; + physicalLoadState[i] = LOAD_OFF; + } + // + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // the primary load is active low. + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // the additional load is active high. + + delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_fasterControl_withRemoteLoad_1.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, calibration values that have supplied in floating point + // form need to be rescaled. + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + energyInBucket_long = 0; + + nominalEnergyThreshold = capacityOfEnergyBucket_long * 0.5; + workingEnergyThreshold_upper = nominalEnergyThreshold; // initial value + workingEnergyThreshold_lower = nominalEnergyThreshold; // initial value + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + + IEU_per_Wh_diverted = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + + Serial.print ("RF capability "); + +#ifdef RF_PRESENT + Serial.print ("IS present, freq = "); + if (freq == RF12_433MHZ) { Serial.println ("433 MHz"); } + if (freq == RF12_868MHZ) { Serial.println ("868 MHz"); } + rf12_initialize(nodeID, freq, networkGroup); // initialize RF +#else + Serial.println ("is NOT present"); +#endif + + Serial.println ("----"); +} + +// An Interrupt Service Routine is now defined in which the ADC is instructed to +// measure each analogue input in sequence. A "data ready" flag is set after each +// voltage conversion has been completed. +// For each set of samples, the two samples for current are taken before the one +// for voltage. This is appropriate because each waveform current is generally slightly +// advanced relative to the waveform for voltage. The data ready flag is cleared +// within loop(). +// This Interrupt Service Routine is for use when the ADC is fixed timer mode. It is +// executed whenever the ADC timer expires. In this mode, the next ADC conversion is +// initiated from within this ISR. +// +void timerIsr(void) +{ + static unsigned char sample_index = 0; + static int sampleI_grid_raw; + static int sampleI_diverted_raw; + + + switch(sample_index) + { + case 0: + sampleV = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current + ADCSRA |= (1< 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + if (beyondStartUpPhase) + { + // a simple routine for checking the performance of the sampling structure + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / sampleSetsDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step would normally be to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient to just + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is generally set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. + + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) + { + realEnergy_diverted = 0; + } + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh_diverted) + { + divertedEnergyRecent_IEU -= IEU_per_Wh_diverted; + divertedEnergyTotal_Wh++; + } + } + + if(perSecondCounter > CYCLES_PER_SECOND) + { + perSecondCounter = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. It's checked every second. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); + } + else + { + perSecondCounter++; + } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; +// Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringNegativeHalfOfMainsCycle = 0; + + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringThisMainsCycle = 0; // not yet dealt with for this cycle + sampleCount_forContinuityChecker = 1; // opportunity has been missed for this cycle + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + if(sampleSetsDuringThisMainsCycle == 5) // to distribute the workload within each mains cycle + { + if (beyondStartUpPhase) + { + // Restrictions apply for the period immediately after a load has been switched. + // Here the recentTransition flag is checked and updated as necessary. + if (recentTransition) + { + postTransitionCount++; + if (postTransitionCount >= POST_TRANSITION_MAX_COUNT) + { + recentTransition = false; + } + } + + if (recentTransition) + { + // During the post-transition period, any change in the energy level is noted. + if (energyInBucket_long > workingEnergyThreshold_upper) + { + workingEnergyThreshold_lower = nominalEnergyThreshold; // reset the opposite threshold + workingEnergyThreshold_upper = energyInBucket_long; + + // the energy thresholds must remain within range + if (workingEnergyThreshold_upper > capacityOfEnergyBucket_long) + { + workingEnergyThreshold_upper = capacityOfEnergyBucket_long; + } + } + else + if (energyInBucket_long < workingEnergyThreshold_lower) + { + workingEnergyThreshold_upper = nominalEnergyThreshold; // reset the opposite threshold + workingEnergyThreshold_lower = energyInBucket_long; + + // the energy thresholds must remain within range + if (workingEnergyThreshold_lower < 0) + { + workingEnergyThreshold_lower = 0; + } + } + } + } + } + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12); + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + // The average power that has been measured during the first half of this mains cycle can now be used + // to predict the energy state at the end of this mains cycle. That prediction will be used to alter + // the state of the load as necessary. The arming signal for the triac can't be set yet - that must + // wait until the voltage has advanced further beyond the -ve going zero-crossing point. + // + long averagePower = sumP_grid / sampleSetsDuringThisMainsCycle;// for 1st half of this mains cycle only + // + // To avoid repetitive and unnecessary calculations, the increase in energy during each mains cycle is + // deemed to be numerically equal to the average power. The predicted value for the energy state at the + // end of this mains cycle will therefore be the known energy state at its start plus the average power + // as measured. Although the average power has been determined over only half a mains cycle, the correct + // number of contributing sample sets has been used so the result can be expected to be a true measurement + // of average power, not half of it. + energyInBucket_prediction = energyInBucket_long + averagePower; // at end of this mains cycle + + + } // end of processing that is specific to the first Vsample in each -ve half cycle + + if (sampleSetsDuringNegativeHalfOfMainsCycle == 5) + { + // the zero-crossing trigger device(s) can now be reliably armed + if (beyondStartUpPhase) + { + /* Determining whether any of the loads need to be changed is is a 3-stage process: + * - change the LOGICAL load states as necessary to maintain the energy level + * - update the PHYSICAL load states according to the logical -> physical mapping + * - update the driver lines for each of the loads. + */ + + if (energyInBucket_prediction > workingEnergyThreshold_upper) + { + // the predicted energy state is high so an extra load may need to be added + boolean OK_toAddLoad = true; // default state of flag + + byte tempLoad = nextLogicalLoadToBeAdded(); + if (tempLoad < noOfDumploads) + { + // a load which is now OFF has been identified for potentially being switched ON + if (recentTransition) + { + if (tempLoad != activeLoad) + { + OK_toAddLoad = false; + } + } + + if (OK_toAddLoad) + { + // tempLoad will be either the active load, or the next load in the priority sequence + logicalLoadState[tempLoad] = LOAD_ON; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + } + } + } + else + if (energyInBucket_prediction < workingEnergyThreshold_lower) + { + // the predicted energy state is low so some load may need to be removed + boolean OK_toRemoveLoad = true; // default state of flag + + byte tempLoad = nextLogicalLoadToBeRemoved(); + if (tempLoad < noOfDumploads) + { + // a load which is now ON has been identified for potentially being switched OFF + if (recentTransition) + { + if (tempLoad != activeLoad) + { + OK_toRemoveLoad = false; + } + } + + if (OK_toRemoveLoad) + { + // tempLoad will be either the active load, or the next load in the priority sequence + logicalLoadState[tempLoad] = LOAD_OFF; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + } + } + } + + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + // update each of the physical loads + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // active high for remote load's LED + + int previousLoadState = tx_data.dumpState; + tx_data.dumpState = physicalLoadState[1]; // for remote load + + if (tx_data.dumpState == previousLoadState) + { + mainsCyclesSinceLastUpdate++; + if (mainsCyclesSinceLastUpdate >= 5) + { + send_rf_data(); + mainsCyclesSinceLastUpdate = 0; + } + } + else + { + send_rf_data(); + mainsCyclesSinceLastUpdate = 0; + } + + // update the Energy Diversion Detector + if (physicalLoadState[0] == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + } + } + + sampleSetsDuringNegativeHalfOfMainsCycle++; + } // end of processing that is specific to samples where the voltage is negative + + // processing for EVERY set of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + // + // extra filtering to offset the HPF effect of CT1 + long last_lpf_long = lpf_long; + lpf_long = last_lpf_long + alpha *(sampleIminusDC_grid - last_lpf_long); + sampleIminusDC_grid += (lpf_gain * lpf_long); + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (x4096, or 2^12) + instP = instP>>12; // reduce to 20-bits (x1) + sumP_grid +=instP; // scaling is x1 + // + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (x4096, or 2^12) + instP = instP>>12; // reduce to 20-bits (x1) + sumP_diverted +=instP; // scaling is x1 + // + + sampleSetsDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + + refreshDisplay(); +} + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count > PERSISTENCE_FOR_POLARITY_CHANGE) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + +byte nextLogicalLoadToBeAdded() +{ + byte retVal = noOfDumploads; + boolean success = false; + for (byte index = 0; index < noOfDumploads && !success; index++) + { + if (logicalLoadState[index] == LOAD_OFF) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +byte nextLogicalLoadToBeRemoved() +{ + byte retVal = noOfDumploads; + boolean success = false; + + // this index counter can't be a 'byte' because the loop would run forever! + // + for (int index = (noOfDumploads -1); index >= 0 && !success; index--) + { + if (logicalLoadState[index] == LOAD_ON) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * The association between the physical and logical loads is 1:1. By default, numerical + * equivalence is maintained, so logical(N) maps to physical(N). If physical load 1 is set + * to have priority, rather than physical load 0, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * Any other mapping relaionships could be configured here. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } +} + + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + +// Serial.println(divertedEnergyTotal_Wh); + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +} + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + // The two versions of the display hardware require different logic. + + // With the pin-saving hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + +#ifdef RF_PRESENT +void send_rf_data() +{ + // rf12_sleep(RF12_WAKEUP); + // if ready to send + exit route if it gets stuck + int i = 0; + while (!rf12_canSend() && i<10) + { + rf12_recvDone(); + i++; + } + rf12_sendNow(0, &tx_data, sizeof tx_data); + + // rf12_sendStart(0, &tx_data, sizeof tx_data); + // rf12_sendWait(2); + // rf12_sleep(RF12_SLEEP); +} +#endif + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_installation.pdf b/docs/routers/mk2pvrouter.co.uk/Mk2_installation.pdf new file mode 100644 index 0000000..5edf282 Binary files /dev/null and b/docs/routers/mk2pvrouter.co.uk/Mk2_installation.pdf differ diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_CAT5_1.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_CAT5_1.ino new file mode 100644 index 0000000..66d7739 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_CAT5_1.ino @@ -0,0 +1,1177 @@ +/* Mk2_multiLoad_CAT5_1.ino + * + * This sketch is for diverting surplus PV power using multiple hard-wired loads. + * An external switch allows either load 0 or Load 1 to have the highest + * priority. Any number of loads can be supported by the logic, a dedicated + * IO pin being required for each one. + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The selector switch, as mentioned above, connects to the "mode" port. For this + * version of the Mk2 code, the system uses a single-threshold version of the + * anti-flicker algorithm which is well suited for multiple loads. As the 'normal' + * mode is no longer required, the 'mode' port has been re-assigned for priority + * selection. + * + * The integral voltage sensor is fed from one of the secondary coils of the transformer. + * Current is measured via Current Transformers at the CT1 and CT2 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the 'diverted' current, so that energy which is diverted via the + * local dump load can be recorded and displayed locally. + * + * A persistence-based 4-digit display is supported. To free up the necessary IO pins + * for driving multiple loads, the pin-saving hardware needs to be in place. + * These extra logic chips (ICs 3 and 4) reduce the number of IO pins + * that are needed to drive the display. The freed-up pins are available at the + * J1-5 connector. The uppermost position has been assigned to drive Load 1, + * the next one down is for Load 2, and the lowest one is for Load 5. The control signal + * for Load 0 is available at the "trigger" connector. + * + * As with all Mk2 PV Router sketches, the output stage is intended to be fed with an + * active-low signal. If active-high control logic is to be used instead (e.g. for + * driving SSRs), an inversion will be required whenever the state of physical IO pin + * is assigned in the code. This can be easily achieved by use of the '!' character. + * + * To drive an SSR rather than a triac, a greater voltage for the control signal is + * required. A transistor stage can be used to switch the unregulated power supply + * to the SSR. + * + * This sketch has many similarities with Revision 5c of the Mk2i PV Router code that I + * have posted on the OpenEnergyMonitor forum. That version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * Robin Emley + * www.Mk2PVrouter.co.uk + * May 2014 + */ + +#include +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define SWEETZONE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +const byte noOfDumploads = 6; // The logic expects a minimum of 2 dumploads, + // for local & remote loads, but neither has to + // be physically present. + +// definitions of enumerated types and instances of same +enum polarities {NEGATIVE, POSITIVE}; +enum outputModes {ANTI_FLICKER, NORMAL}; +enum loadPriorityModes {LOAD_1_HAS_PRIORITY, LOAD_0_HAS_PRIORITY}; + +enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are active low +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +enum energyStates {LOWER_HALF, UPPER_HALF}; // for single threshold AF algorithm +enum energyStates energyStateNow; + +// ---------------- Extra Features selection ---------------------- +// +// - WORKLOAD_CHECK, for determining how much spare processing time there is. +// +// #define WORKLOAD_CHECK // <-- Include this line is this feature is required + + +// For most single-load Mk2 systems, the power-diversion logic can operate in either of two modes: +// +// - NORMAL, where the triac switches rapidly on/off to maintain a constant energy level. +// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations +// of the local mains voltage. +// +// For this multi-load version, the same mechanism has been retained but the +// output mode is hard-coded as below: +enum outputModes outputMode = ANTI_FLICKER; + +// In this multi-load version, the external switch is re-used to determine the load priority +enum loadPriorityModes loadPriorityMode = LOAD_0_HAS_PRIORITY; + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +const byte physicalLoad_2_pin = 2; // <-- to control an additional load +const byte loadPrioritySelectorPin = 3; // <-- this is the "mode" port +const byte physicalLoad_0_pin = 4; // <-- this is the "trigger" port +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +const byte physicalLoad_3_pin = 10; // <-- to control an additional load +const byte physicalLoad_5_pin = 11; // <-- to control an additional load +const byte physicalLoad_1_pin = 12; // <-- to control an additional load +const byte physicalLoad_4_pin = 13; // <-- to control an additional load + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + + +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +long cycleCount = 0; // counts mains cycles from start-up +long energyInBucket_long = 0; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long energyThreshold_long; // used for 'normal' and single-threshold 'AF' logic +// int phaseCal_grid_int; // to avoid the need for floating-point maths +// int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; + +int postMidPointCrossingDelay_cycles; // assigned in setup(), different for each output mode +const int postMidPointCrossingDelayForAF_cycles = 25; // in 20 ms counts +const int interLoadSeparationDelay_cycles = 25; // in 20 ms cycle counts (for both output modes) +byte activeLoadID; // only one load may operate freely at a time. +long cycleCountAtLastMidPointCrossing = 0; +long cycleCountAtLastTransition = 0; +long energyAtLastOffTransition_long; +long energyAtLastOnTransition_long; + + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +int sampleI_grid; +int sampleI_diverted; +int sampleV; + + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// With most hardware, the default values are likely to work fine without +// need for change. A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.044; // for CT2 + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When +// measuring a resistive load, the voltage and current waveforms should be perfectly +// aligned. In this situation, the Power Factor will be 1. +// +// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct +// value of phaseCal for any hardware configuration. An index of my various Mk2-related +// exhibits is available at http://openenergymonitor.org/emon/node/1757 +// +//const float phaseCal_grid = 1.0; <--- not used in this version +//const float phaseCal_diverted = 1.0; <--- not used in this version + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 10 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // Energy Diversion Detection (for the local load only) +long requiredExportPerMainsCycle_inIEU; +float IEUtoJoulesConversion_CT1; + +void setup() +{ + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for the local dump-load + pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_2_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_3_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_4_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_5_pin, OUTPUT); // driver pin for an additional load + + for(int i = 0; i< noOfDumploads; i++) + { + logicalLoadState[i] = LOAD_OFF; + physicalLoadState[i] = LOAD_OFF; + } + + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // update the local load's state. + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // update the additional load state (inverse logic). + digitalWrite(physicalLoad_2_pin, !physicalLoadState[2]); // update the additional load state (inverse logic). + digitalWrite(physicalLoad_3_pin, !physicalLoadState[3]); // update the additional load state (inverse logic). + digitalWrite(physicalLoad_4_pin, !physicalLoadState[4]); // update the additional load state (inverse logic). + digitalWrite(physicalLoad_5_pin, !physicalLoadState[5]); // update the additional load state (inverse logic). + + pinMode(loadPrioritySelectorPin, INPUT); // this pin is tracked to the "mode" connector + digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and + loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority + + delay(5000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_multiLoad_CAT5_1.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, calibration values that are supplied in floating point + // form need to be rescaled. + // +// phaseCal_grid_int = phaseCal_grid * 256; // for integer maths <-- not supported +// phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths <-- not supported + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); +// energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up + + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the correct scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples. The main processor and +// the ADC work autonomously, their operation being only linked via the dataReady flag. +// As soon as a new set of data is made available by the ADC, the main processor can +// start to work on it immediately. +// +void allGeneralProcessing() +{ + static boolean beyondStartUpPhase = false; // start-up delay, allows things to settle + static boolean triggerNeedsToBeArmed = false; // once per mains cycle (+ve half) + static int samplesDuringThisMainsCycle; // for normalising the power in each mains cycle + static long sumP_grid; // for per-cycle summation of 'real power' + static long sumP_diverted; // for per-cycle summation of 'real power' + static enum polarities polarityOfLastSampleV; // for zero-crossing detection + static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) + static long lastSampleVminusDC_long; // for the phaseCal algorithm + static byte perSecondCounter = 0; + + // remove DC offset from the raw voltage sample by subtracting the accurate value + // as determined by a LP filter. + long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long; + + // determine the polarity of the latest voltage sample + enum polarities polarityNow; + if(sampleVminusDC_long > 0) { + polarityNow = POSITIVE; } + else { + polarityNow = NEGATIVE; } + + if (polarityNow == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + cycleCount++; + + triggerNeedsToBeArmed = true; // the trigger is armed once during each +ve half-cycle + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / samplesDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / samplesDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- for non-standard use + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + if (EDD_isActive) // Energy Diversion Display + { + // When locally diverted energy is being monitored, the latest contribution + // needs to be added to an accumulator which operates with maximum precision. + // + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) { + realEnergy_diverted = 0; } // to avoid 'creep' + + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + perSecondCounter++; + if(perSecondCounter >= CYCLES_PER_SECOND) + { + perSecondCounter = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); // occurs every second + + Serial.print (energyInBucket_long * IEUtoJoulesConversion_CT1); + Serial.print (", "); + for (byte loadID = 0; loadID < noOfDumploads; loadID++) + { + Serial.print (!physicalLoadState[loadID]); // 1 = "on", 0 = "off" + Serial.print (" "); + } + Serial.println(); + } + + + // when using the single-threshold power diversion algorithm, a counter needs + // to be reset whenever the energy level in the accumulator crosses the mid-point + // + enum energyStates energyStateOnLastLoop = energyStateNow; + + if (energyInBucket_long > energyThreshold_long) { + energyStateNow = UPPER_HALF; } + else { + energyStateNow = LOWER_HALF; } + + if (energyStateNow != energyStateOnLastLoop) { + cycleCountAtLastMidPointCrossing = cycleCount; } + + + // clear the per-cycle accumulators for use in this new mains cycle. + samplesDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + if (triggerNeedsToBeArmed == true) + { + // check to see whether the trigger device can now be reliably armed + if(samplesDuringThisMainsCycle == 3) // should always exceed 20V (the min for trigger) + { + /* Now it's time to determine whether any of the the loads need to be changed. + * This is a 2-stage process: + * First, change the LOGICAL loads as necessary, then update the PHYSICAL + * loads according to the mapping that exists between them. The mapping is + * 1:1 by default but can be altered by a hardware switch which allows the + * priority of the remote load to be altered. + * This code uses a single-threshold algorithm which relies on regular switching + * of the load. + */ + if (cycleCount > cycleCountAtLastMidPointCrossing + postMidPointCrossingDelay_cycles) + { + if (energyInBucket_long > energyThreshold_long) + { + increaseLoadIfPossible(); // to reduce the level in the bucket + } + else + { + decreaseLoadIfPossible(); // to increase the level in the bucket + } + } + + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + // update all the physical loads + digitalWrite(physicalLoad_0_pin, !physicalLoadState[0]); // active high for SSR + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // active high for SSR + digitalWrite(physicalLoad_2_pin, !physicalLoadState[2]); // active high for SSR + digitalWrite(physicalLoad_3_pin, !physicalLoadState[3]); // active high for SSR + digitalWrite(physicalLoad_4_pin, !physicalLoadState[4]); // active high for SSR + digitalWrite(physicalLoad_5_pin, !physicalLoadState[5]); // active high for SSR + + // update the Energy Diversion Detector + if (physicalLoadState[0] == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + + // clear the flag which ensures that loads are only updated once per mains cycle + triggerNeedsToBeArmed = false; + } + } + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > startUpPeriod * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + samplesDuringThisMainsCycle = 0; + Serial.println ("Go!"); + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + +// checkOutputModeSelection(); // updates outputMode if switch is changed + checkLoadPrioritySelection(); // updates load priorities if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is positive + + // processing for EVERY pair of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform +// long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform +// long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + samplesDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityOfLastSampleV = polarityNow; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + + +void increaseLoadIfPossible() +{ + /* if permitted by A/F rules, turn on the highest priority logical load that is not already on. + */ + boolean changed = false; + + // Only one load may operate freely at a time. Other loads are prevented from + // switching until a sufficient period has elapsed since the last transition, and + // then only if the energy level has not have fallen since the previous ON transition. + // These measures allow a lower priority load to contribute if a higher priority + // load is not having the desired effect, but not immediately. + // + if (energyInBucket_long >= energyAtLastOnTransition_long) + { + boolean timeout = (cycleCount > cycleCountAtLastTransition + interLoadSeparationDelay_cycles); + for (int i = 0; i < noOfDumploads && !changed; i++) + { + if (logicalLoadState[i] == LOAD_OFF) + { + if ((i == activeLoadID) || timeout) + { + logicalLoadState[i] = LOAD_ON; + cycleCountAtLastTransition = cycleCount; + energyAtLastOnTransition_long = energyInBucket_long; +// energyAtLastOffTransition_long = 3600; // reset the 'opposite' mechanism. <-WRONG! + energyAtLastOffTransition_long = capacityOfEnergyBucket_long; // reset the 'opposite' mechanism. + activeLoadID = i; + changed = true; + } + } + } + } + else + { + // energy level has not risen so there's no need to apply any more load + } +// return (changed); +} + +void decreaseLoadIfPossible() +{ + /* if permitted by A/F rules, turn off the highest priority logical load that is not already off. + */ + boolean changed = false; + + // Only one load may operate freely at a time. Other loads are prevented from + // switching until a sufficient period has elapsed since the last transition, and + // then only if the energy level has not have risen since the previous OFF transition. + // These measures allow a lower priority load to contribute if a higher priority + // load is not having the desired effect, but not immediately. + // + if (energyInBucket_long <= energyAtLastOnTransition_long) + { + boolean timeout = (cycleCount > cycleCountAtLastTransition + interLoadSeparationDelay_cycles); +// for (int i = 0; i < noOfDumploads && !done; i++) + for (int i = (noOfDumploads -1); i >= 0 && !changed; i--) + { + if (logicalLoadState[i] == LOAD_ON) + { + if ((i == activeLoadID) || timeout) + { + logicalLoadState[i] = LOAD_OFF; + cycleCountAtLastTransition = cycleCount; + energyAtLastOnTransition_long = energyInBucket_long; + energyAtLastOffTransition_long = 0; // reset the 'opposite' mechanism. + activeLoadID = i; + changed = true; + } + } + } + } + else + { + // energy level has not fallen so there's no need to apply any more load + } +// return (changed); +} + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * By default, the association between the physical and logical loads is 1:1. If + * the physical load 1 is set to have priority, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * For this implementation, all loads are 'local' because the RF facility is not in use. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } + + if (loadPriorityMode == LOAD_1_HAS_PRIORITY) + { + // swap physical loads 0 & 1 if remote load has priority + physicalLoadState[0] = logicalLoadState[1]; + physicalLoadState[1] = logicalLoadState[0]; + } +} + + +// this function changes the value of the load priorities if the state of the external switch is altered +void checkLoadPrioritySelection() +{ + static byte loadPrioritySwitchCcount = 0; + int pinState = digitalRead(loadPrioritySelectorPin); + if (pinState != loadPriorityMode) + { + loadPrioritySwitchCcount++; + } + if (loadPrioritySwitchCcount >= 20) + { + loadPrioritySwitchCcount = 0; + loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable + Serial.print ("loadPriority selection changed to "); + if (loadPriorityMode == LOAD_0_HAS_PRIORITY) { + Serial.println ( "load 0"); } + else { + Serial.println ( "load 1"); } + } +} + + +// Although this sketch always operates in ANTI_FLICKER mode, it was convenient +// to leave this mechanism in place. +// +void configureParamsForSelectedOutputMode() +{ + energyThreshold_long = capacityOfEnergyBucket_long * 0.5; + + if (outputMode == ANTI_FLICKER) + { + postMidPointCrossingDelay_cycles = postMidPointCrossingDelayForAF_cycles; + } + else + { + postMidPointCrossingDelay_cycles = 0; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" energyThreshold_long = "); + Serial.println(energyThreshold_long); + Serial.print(" postMidPointCrossingDelay_cycles = "); + Serial.println(postMidPointCrossingDelay_cycles); + Serial.print(" interLoadSeparationDelay_cycles = "); + Serial.println(interLoadSeparationDelay_cycles); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +// valueToBeDisplayed++; +} + + + + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_CAT5_2.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_CAT5_2.ino new file mode 100644 index 0000000..57cd59e --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_CAT5_2.ino @@ -0,0 +1,1177 @@ +/* Mk2_multiLoad_CAT5_2.ino + * + * This sketch is for diverting surplus PV power using multiple hard-wired loads. + * An external switch allows either load 0 or Load 1 to have the highest + * priority. Any number of loads can be supported by the logic, a dedicated + * IO pin being required for each one. + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The selector switch, as mentioned above, connects to the "mode" port. For this + * version of the Mk2 code, the system uses a single-threshold version of the + * anti-flicker algorithm which is well suited for multiple loads. As the 'normal' + * mode is no longer required, the 'mode' port has been re-assigned for priority + * selection. + * + * The integral voltage sensor is fed from one of the secondary coils of the transformer. + * Current is measured via Current Transformers at the CT1 and CT2 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the 'diverted' current, so that energy which is diverted via the + * local dump load can be recorded and displayed locally. + * + * A persistence-based 4-digit display is supported. To free up the necessary IO pins + * for driving multiple loads, the pin-saving hardware needs to be in place. + * These extra logic chips (ICs 3 and 4) reduce the number of IO pins + * that are needed to drive the display. The freed-up pins are available at the + * J1-5 connector. The uppermost position has been assigned to drive Load 1, + * the next one down is for Load 2, and the lowest one is for Load 5. The control signal + * for Load 0 is available at the "trigger" connector. + * + * As with all Mk2 PV Router sketches, the output stage is intended to be fed with an + * active-low signal. If active-high control logic is to be used instead (e.g. for + * driving SSRs), an inversion will be required whenever the state of physical IO pin + * is assigned in the code. This can be easily achieved by use of the '!' character. + * + * To drive an SSR rather than a triac, a greater voltage for the control signal is + * required. A transistor stage can be used to switch the unregulated power supply + * to the SSR. + * + * This sketch has many similarities with Revision 5c of the Mk2i PV Router code that I + * have posted on the OpenEnergyMonitor forum. That version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * Robin Emley + * www.Mk2PVrouter.co.uk + * May 2014 + */ + +#include +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define SWEETZONE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +const byte noOfDumploads = 6; // The logic expects a minimum of 2 dumploads, + // for local & remote loads, but neither has to + // be physically present. + +// definitions of enumerated types and instances of same +enum polarities {NEGATIVE, POSITIVE}; +enum outputModes {ANTI_FLICKER, NORMAL}; +enum loadPriorityModes {LOAD_1_HAS_PRIORITY, LOAD_0_HAS_PRIORITY}; + +enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are active low +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +enum energyStates {LOWER_HALF, UPPER_HALF}; // for single threshold AF algorithm +enum energyStates energyStateNow; + +// ---------------- Extra Features selection ---------------------- +// +// - WORKLOAD_CHECK, for determining how much spare processing time there is. +// +// #define WORKLOAD_CHECK // <-- Include this line is this feature is required + + +// For most single-load Mk2 systems, the power-diversion logic can operate in either of two modes: +// +// - NORMAL, where the triac switches rapidly on/off to maintain a constant energy level. +// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations +// of the local mains voltage. +// +// For this multi-load version, the same mechanism has been retained but the +// output mode is hard-coded as below: +enum outputModes outputMode = ANTI_FLICKER; + +// In this multi-load version, the external switch is re-used to determine the load priority +enum loadPriorityModes loadPriorityMode = LOAD_0_HAS_PRIORITY; + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +const byte physicalLoad_2_pin = 2; // <-- to control an additional load +const byte loadPrioritySelectorPin = 3; // <-- this is the "mode" port +const byte physicalLoad_0_pin = 4; // <-- this is the "trigger" port +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +const byte physicalLoad_3_pin = 10; // <-- to control an additional load +const byte physicalLoad_5_pin = 11; // <-- to control an additional load +const byte physicalLoad_1_pin = 12; // <-- to control an additional load +const byte physicalLoad_4_pin = 13; // <-- to control an additional load + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + + +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +long cycleCount = 0; // counts mains cycles from start-up +long energyInBucket_long = 0; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long energyThreshold_long; // used for 'normal' and single-threshold 'AF' logic +// int phaseCal_grid_int; // to avoid the need for floating-point maths +// int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; + +int postMidPointCrossingDelay_cycles; // assigned in setup(), different for each output mode +const int postMidPointCrossingDelayForAF_cycles = 25; // in 20 ms counts +const int interLoadSeparationDelay_cycles = 25; // in 20 ms cycle counts (for both output modes) +byte activeLoadID; // only one load may operate freely at a time. +long cycleCountAtLastMidPointCrossing = 0; +long cycleCountAtLastTransition = 0; +long energyAtLastOffTransition_long; +long energyAtLastOnTransition_long; + + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +int sampleI_grid; +int sampleI_diverted; +int sampleV; + + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// With most hardware, the default values are likely to work fine without +// need for change. A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.044; // for CT2 + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When +// measuring a resistive load, the voltage and current waveforms should be perfectly +// aligned. In this situation, the Power Factor will be 1. +// +// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct +// value of phaseCal for any hardware configuration. An index of my various Mk2-related +// exhibits is available at http://openenergymonitor.org/emon/node/1757 +// +//const float phaseCal_grid = 1.0; <--- not used in this version +//const float phaseCal_diverted = 1.0; <--- not used in this version + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 10 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // Energy Diversion Detection (for the local load only) +long requiredExportPerMainsCycle_inIEU; +float IEUtoJoulesConversion_CT1; + +void setup() +{ + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for the local dump-load + pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_2_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_3_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_4_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_5_pin, OUTPUT); // driver pin for an additional load + + for(int i = 0; i< noOfDumploads; i++) + { + logicalLoadState[i] = LOAD_OFF; + physicalLoadState[i] = LOAD_OFF; + } + + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // update the local load's state (active low). + digitalWrite(physicalLoad_1_pin, physicalLoadState[1]); // update the additional load state (active low). + digitalWrite(physicalLoad_2_pin, physicalLoadState[2]); // update the additional load state (active low). + digitalWrite(physicalLoad_3_pin, physicalLoadState[3]); // update the additional load state (active low)). + digitalWrite(physicalLoad_4_pin, physicalLoadState[4]); // update the additional load state (active low). + digitalWrite(physicalLoad_5_pin, physicalLoadState[5]); // update the additional load state (active low). + + pinMode(loadPrioritySelectorPin, INPUT); // this pin is tracked to the "mode" connector + digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and + loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority + + delay(5000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_multiLoad_CAT5_2.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, calibration values that are supplied in floating point + // form need to be rescaled. + // +// phaseCal_grid_int = phaseCal_grid * 256; // for integer maths <-- not supported +// phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths <-- not supported + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); +// energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up + + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the correct scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples. The main processor and +// the ADC work autonomously, their operation being only linked via the dataReady flag. +// As soon as a new set of data is made available by the ADC, the main processor can +// start to work on it immediately. +// +void allGeneralProcessing() +{ + static boolean beyondStartUpPhase = false; // start-up delay, allows things to settle + static boolean triggerNeedsToBeArmed = false; // once per mains cycle (+ve half) + static int samplesDuringThisMainsCycle; // for normalising the power in each mains cycle + static long sumP_grid; // for per-cycle summation of 'real power' + static long sumP_diverted; // for per-cycle summation of 'real power' + static enum polarities polarityOfLastSampleV; // for zero-crossing detection + static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) + static long lastSampleVminusDC_long; // for the phaseCal algorithm + static byte perSecondCounter = 0; + + // remove DC offset from the raw voltage sample by subtracting the accurate value + // as determined by a LP filter. + long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long; + + // determine the polarity of the latest voltage sample + enum polarities polarityNow; + if(sampleVminusDC_long > 0) { + polarityNow = POSITIVE; } + else { + polarityNow = NEGATIVE; } + + if (polarityNow == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + cycleCount++; + + triggerNeedsToBeArmed = true; // the trigger is armed once during each +ve half-cycle + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / samplesDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / samplesDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- for non-standard use + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + if (EDD_isActive) // Energy Diversion Display + { + // When locally diverted energy is being monitored, the latest contribution + // needs to be added to an accumulator which operates with maximum precision. + // + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) { + realEnergy_diverted = 0; } // to avoid 'creep' + + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + perSecondCounter++; + if(perSecondCounter >= CYCLES_PER_SECOND) + { + perSecondCounter = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); // occurs every second + + Serial.print (energyInBucket_long * IEUtoJoulesConversion_CT1); + Serial.print (", "); + for (byte loadID = 0; loadID < noOfDumploads; loadID++) + { + Serial.print (!physicalLoadState[loadID]); // 1 = "on", 0 = "off" + Serial.print (" "); + } + Serial.println(); + } + + + // when using the single-threshold power diversion algorithm, a counter needs + // to be reset whenever the energy level in the accumulator crosses the mid-point + // + enum energyStates energyStateOnLastLoop = energyStateNow; + + if (energyInBucket_long > energyThreshold_long) { + energyStateNow = UPPER_HALF; } + else { + energyStateNow = LOWER_HALF; } + + if (energyStateNow != energyStateOnLastLoop) { + cycleCountAtLastMidPointCrossing = cycleCount; } + + + // clear the per-cycle accumulators for use in this new mains cycle. + samplesDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + if (triggerNeedsToBeArmed == true) + { + // check to see whether the trigger device can now be reliably armed + if(samplesDuringThisMainsCycle == 3) // should always exceed 20V (the min for trigger) + { + /* Now it's time to determine whether any of the the loads need to be changed. + * This is a 2-stage process: + * First, change the LOGICAL loads as necessary, then update the PHYSICAL + * loads according to the mapping that exists between them. The mapping is + * 1:1 by default but can be altered by a hardware switch which allows the + * priority of the remote load to be altered. + * This code uses a single-threshold algorithm which relies on regular switching + * of the load. + */ + if (cycleCount > cycleCountAtLastMidPointCrossing + postMidPointCrossingDelay_cycles) + { + if (energyInBucket_long > energyThreshold_long) + { + increaseLoadIfPossible(); // to reduce the level in the bucket + } + else + { + decreaseLoadIfPossible(); // to increase the level in the bucket + } + } + + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + // update all the physical loads + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger + digitalWrite(physicalLoad_1_pin, physicalLoadState[1]); // active low for trigger + digitalWrite(physicalLoad_2_pin, physicalLoadState[2]); // active low for trigger + digitalWrite(physicalLoad_3_pin, physicalLoadState[3]); // active low for trigger + digitalWrite(physicalLoad_4_pin, physicalLoadState[4]); // active low for trigger + digitalWrite(physicalLoad_5_pin, physicalLoadState[5]); // active low for trigger + + // update the Energy Diversion Detector + if (physicalLoadState[0] == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + + // clear the flag which ensures that loads are only updated once per mains cycle + triggerNeedsToBeArmed = false; + } + } + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > startUpPeriod * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + samplesDuringThisMainsCycle = 0; + Serial.println ("Go!"); + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + +// checkOutputModeSelection(); // updates outputMode if switch is changed + checkLoadPrioritySelection(); // updates load priorities if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is positive + + // processing for EVERY pair of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform +// long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform +// long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + samplesDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityOfLastSampleV = polarityNow; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + + +void increaseLoadIfPossible() +{ + /* if permitted by A/F rules, turn on the highest priority logical load that is not already on. + */ + boolean changed = false; + + // Only one load may operate freely at a time. Other loads are prevented from + // switching until a sufficient period has elapsed since the last transition, and + // then only if the energy level has not have fallen since the previous ON transition. + // These measures allow a lower priority load to contribute if a higher priority + // load is not having the desired effect, but not immediately. + // + if (energyInBucket_long >= energyAtLastOnTransition_long) + { + boolean timeout = (cycleCount > cycleCountAtLastTransition + interLoadSeparationDelay_cycles); + for (int i = 0; i < noOfDumploads && !changed; i++) + { + if (logicalLoadState[i] == LOAD_OFF) + { + if ((i == activeLoadID) || timeout) + { + logicalLoadState[i] = LOAD_ON; + cycleCountAtLastTransition = cycleCount; + energyAtLastOnTransition_long = energyInBucket_long; +// energyAtLastOffTransition_long = 3600; // reset the 'opposite' mechanism. <-WRONG! + energyAtLastOffTransition_long = capacityOfEnergyBucket_long; // reset the 'opposite' mechanism. + activeLoadID = i; + changed = true; + } + } + } + } + else + { + // energy level has not risen so there's no need to apply any more load + } +// return (changed); +} + +void decreaseLoadIfPossible() +{ + /* if permitted by A/F rules, turn off the highest priority logical load that is not already off. + */ + boolean changed = false; + + // Only one load may operate freely at a time. Other loads are prevented from + // switching until a sufficient period has elapsed since the last transition, and + // then only if the energy level has not have risen since the previous OFF transition. + // These measures allow a lower priority load to contribute if a higher priority + // load is not having the desired effect, but not immediately. + // + if (energyInBucket_long <= energyAtLastOnTransition_long) + { + boolean timeout = (cycleCount > cycleCountAtLastTransition + interLoadSeparationDelay_cycles); +// for (int i = 0; i < noOfDumploads && !done; i++) + for (int i = (noOfDumploads -1); i >= 0 && !changed; i--) + { + if (logicalLoadState[i] == LOAD_ON) + { + if ((i == activeLoadID) || timeout) + { + logicalLoadState[i] = LOAD_OFF; + cycleCountAtLastTransition = cycleCount; + energyAtLastOnTransition_long = energyInBucket_long; + energyAtLastOffTransition_long = 0; // reset the 'opposite' mechanism. + activeLoadID = i; + changed = true; + } + } + } + } + else + { + // energy level has not fallen so there's no need to apply any more load + } +// return (changed); +} + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * By default, the association between the physical and logical loads is 1:1. If + * the physical load 1 is set to have priority, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * For this implementation, all loads are 'local' because the RF facility is not in use. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } + + if (loadPriorityMode == LOAD_1_HAS_PRIORITY) + { + // swap physical loads 0 & 1 if remote load has priority + physicalLoadState[0] = logicalLoadState[1]; + physicalLoadState[1] = logicalLoadState[0]; + } +} + + +// this function changes the value of the load priorities if the state of the external switch is altered +void checkLoadPrioritySelection() +{ + static byte loadPrioritySwitchCcount = 0; + int pinState = digitalRead(loadPrioritySelectorPin); + if (pinState != loadPriorityMode) + { + loadPrioritySwitchCcount++; + } + if (loadPrioritySwitchCcount >= 20) + { + loadPrioritySwitchCcount = 0; + loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable + Serial.print ("loadPriority selection changed to "); + if (loadPriorityMode == LOAD_0_HAS_PRIORITY) { + Serial.println ( "load 0"); } + else { + Serial.println ( "load 1"); } + } +} + + +// Although this sketch always operates in ANTI_FLICKER mode, it was convenient +// to leave this mechanism in place. +// +void configureParamsForSelectedOutputMode() +{ + energyThreshold_long = capacityOfEnergyBucket_long * 0.5; + + if (outputMode == ANTI_FLICKER) + { + postMidPointCrossingDelay_cycles = postMidPointCrossingDelayForAF_cycles; + } + else + { + postMidPointCrossingDelay_cycles = 0; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" energyThreshold_long = "); + Serial.println(energyThreshold_long); + Serial.print(" postMidPointCrossingDelay_cycles = "); + Serial.println(postMidPointCrossingDelay_cycles); + Serial.print(" interLoadSeparationDelay_cycles = "); + Serial.println(interLoadSeparationDelay_cycles); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +// valueToBeDisplayed++; +} + + + + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_CAT5_3.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_CAT5_3.ino new file mode 100644 index 0000000..a043fad --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_CAT5_3.ino @@ -0,0 +1,1185 @@ +/* Mk2_multiLoad_CAT5_3.ino + * + * This sketch is for diverting surplus PV power using multiple hard-wired loads. + * An external switch allows either load 0 or Load 1 to have the highest + * priority. Any number of loads can be supported by the logic, a dedicated + * IO pin being required for each one. + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The selector switch, as mentioned above, connects to the "mode" port. For this + * version of the Mk2 code, the system uses a single-threshold version of the + * anti-flicker algorithm which is well suited for multiple loads. As the 'normal' + * mode is no longer required, the 'mode' port has been re-assigned for priority + * selection. + * + * The integral voltage sensor is fed from one of the secondary coils of the transformer. + * Current is measured via Current Transformers at the CT1 and CT2 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the 'diverted' current, so that energy which is diverted via the + * local dump load can be recorded and displayed locally. + * + * A persistence-based 4-digit display is supported. To free up the necessary IO pins + * for driving multiple loads, the pin-saving hardware needs to be in place. + * These extra logic chips (ICs 3 and 4) reduce the number of IO pins + * that are needed to drive the display. The freed-up pins are available at the + * J1-5 connector. The uppermost position has been assigned to drive Load 1, + * the next one down is for Load 2, and the lowest one is for Load 5. The control signal + * for Load 0 is available at the "trigger" connector. + * + * As with all Mk2 PV Router sketches, the output stage is intended to be fed with an + * active-low signal. If active-high control logic is to be used instead (e.g. for + * driving SSRs), an inversion will be required whenever the state of physical IO pin + * is assigned in the code. This can be easily achieved by use of the '!' character. + * + * To drive an SSR rather than a triac, a greater voltage for the control signal is + * required. A transistor stage can be used to switch the unregulated power supply + * to the SSR. + * + * This sketch has many similarities with Revision 5c of the Mk2i PV Router code that I + * have posted on the OpenEnergyMonitor forum. That version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * September 2014: renamed as Mk2_multiLoad_CAT5_3, with these changes: + * - reimplementation of cycleCount, as it could have overflowed with unpredictable results; + * - the functions increaseLoadIfPossible() and decreaseLoadIfPossible() have been tidied; + * - energyThreshold_long has been renamed as midPointOfEnergyBucket_long. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + * September 2014 + */ + +#include +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define SWEETZONE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +const byte noOfDumploads = 6; // The logic expects a minimum of 2 dumploads, + // for local & remote loads, but neither has to + // be physically present. + +// definitions of enumerated types and instances of same +enum polarities {NEGATIVE, POSITIVE}; +enum outputModes {ANTI_FLICKER, NORMAL}; +enum loadPriorityModes {LOAD_1_HAS_PRIORITY, LOAD_0_HAS_PRIORITY}; + +enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are active low +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +enum energyStates {LOWER_HALF, UPPER_HALF}; // for single threshold AF algorithm +enum energyStates energyStateNow; + +// ---------------- Extra Features selection ---------------------- +// +// - WORKLOAD_CHECK, for determining how much spare processing time there is. +// +// #define WORKLOAD_CHECK // <-- Include this line is this feature is required + + +// For most single-load Mk2 systems, the power-diversion logic can operate in either of two modes: +// +// - NORMAL, where the triac switches rapidly on/off to maintain a constant energy level. +// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations +// of the local mains voltage. +// +// For this multi-load version, the same mechanism has been retained but the +// output mode is hard-coded as below: +enum outputModes outputMode = ANTI_FLICKER; + +// In this multi-load version, the external switch is re-used to determine the load priority +enum loadPriorityModes loadPriorityMode = LOAD_0_HAS_PRIORITY; + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +const byte physicalLoad_2_pin = 2; // <-- to control an additional load +const byte loadPrioritySelectorPin = 3; // <-- this is the "mode" port +const byte physicalLoad_0_pin = 4; // <-- this is the "trigger" port +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +const byte physicalLoad_3_pin = 10; // <-- to control an additional load +const byte physicalLoad_5_pin = 11; // <-- to control an additional load +const byte physicalLoad_1_pin = 12; // <-- to control an additional load +const byte physicalLoad_4_pin = 13; // <-- to control an additional load + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + + +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +long energyInBucket_long = 0; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long midPointOfEnergyBucket_long; // used for 'normal' and single-threshold 'AF' logic +// int phaseCal_grid_int; // to avoid the need for floating-point maths +// int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; + +int postMidPointCrossingDelay_cycles; // assigned in setup(), different for each output mode +const int postMidPointCrossingDelayForAF_cycles = 25; // in 20 ms counts +const int interLoadSeparationDelay_cycles = 25; // in 20 ms cycle counts (for both output modes) +byte activeLoadID; // only one load may operate freely at a time. + +long energyAtLastOffTransition_long; +long energyAtLastOnTransition_long; +int mainsCyclesSinceLastMidPointCrossing = 0; +int mainsCyclesSinceLastChangeOfLoadState = 0; + + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +int sampleI_grid; +int sampleI_diverted; +int sampleV; + + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// With most hardware, the default values are likely to work fine without +// need for change. A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.044; // for CT2 + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When +// measuring a resistive load, the voltage and current waveforms should be perfectly +// aligned. In this situation, the Power Factor will be 1. +// +// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct +// value of phaseCal for any hardware configuration. An index of my various Mk2-related +// exhibits is available at http://openenergymonitor.org/emon/node/1757 +// +//const float phaseCal_grid = 1.0; <--- not used in this version +//const float phaseCal_diverted = 1.0; <--- not used in this version + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 10 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // Energy Diversion Detection (for the local load only) +long requiredExportPerMainsCycle_inIEU; +float IEUtoJoulesConversion_CT1; + +void setup() +{ + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for the local dump-load + pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_2_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_3_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_4_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_5_pin, OUTPUT); // driver pin for an additional load + + for(int i = 0; i< noOfDumploads; i++) + { + logicalLoadState[i] = LOAD_OFF; + physicalLoadState[i] = LOAD_OFF; + } + + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // update the local load's state (active low). + digitalWrite(physicalLoad_1_pin, physicalLoadState[1]); // update the additional load state (active low). + digitalWrite(physicalLoad_2_pin, physicalLoadState[2]); // update the additional load state (active low). + digitalWrite(physicalLoad_3_pin, physicalLoadState[3]); // update the additional load state (active low)). + digitalWrite(physicalLoad_4_pin, physicalLoadState[4]); // update the additional load state (active low). + digitalWrite(physicalLoad_5_pin, physicalLoadState[5]); // update the additional load state (active low). + + pinMode(loadPrioritySelectorPin, INPUT); // this pin is tracked to the "mode" connector + digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and + loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority + + delay(5000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_multiLoad_CAT5_3.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, calibration values that are supplied in floating point + // form need to be rescaled. + // +// phaseCal_grid_int = phaseCal_grid * 256; // for integer maths <-- not supported +// phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths <-- not supported + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); +// energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up + midPointOfEnergyBucket_long = capacityOfEnergyBucket_long / 2; + energyAtLastOffTransition_long = midPointOfEnergyBucket_long; + energyAtLastOnTransition_long = midPointOfEnergyBucket_long; + + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the correct scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples. The main processor and +// the ADC work autonomously, their operation being only linked via the dataReady flag. +// As soon as a new set of data is made available by the ADC, the main processor can +// start to work on it immediately. +// +void allGeneralProcessing() +{ + static boolean beyondStartUpPhase = false; // start-up delay, allows things to settle + static boolean triggerNeedsToBeArmed = false; // once per mains cycle (+ve half) + static int samplesDuringThisMainsCycle; // for normalising the power in each mains cycle + static long sumP_grid; // for per-cycle summation of 'real power' + static long sumP_diverted; // for per-cycle summation of 'real power' + static enum polarities polarityOfLastSampleV; // for zero-crossing detection + static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) + static long lastSampleVminusDC_long; // for the phaseCal algorithm + static byte perSecondCounter = 0; + + // remove DC offset from the raw voltage sample by subtracting the accurate value + // as determined by a LP filter. + long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long; + + // determine the polarity of the latest voltage sample + enum polarities polarityNow; + if(sampleVminusDC_long > 0) { + polarityNow = POSITIVE; } + else { + polarityNow = NEGATIVE; } + + if (polarityNow == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + // cycleCount++; <- this mechanism is unsafe, it will eventually overflow + mainsCyclesSinceLastMidPointCrossing++; + mainsCyclesSinceLastChangeOfLoadState++; + + triggerNeedsToBeArmed = true; // the trigger is armed once during each +ve half-cycle + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / samplesDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / samplesDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- for non-standard use + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + if (EDD_isActive) // Energy Diversion Display + { + // When locally diverted energy is being monitored, the latest contribution + // needs to be added to an accumulator which operates with maximum precision. + // + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) { + realEnergy_diverted = 0; } // to avoid 'creep' + + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + perSecondCounter++; + if(perSecondCounter >= CYCLES_PER_SECOND) + { + perSecondCounter = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); // occurs every second + + Serial.print (energyInBucket_long * IEUtoJoulesConversion_CT1); + Serial.print (", "); + for (byte loadID = 0; loadID < noOfDumploads; loadID++) + { + Serial.print (!physicalLoadState[loadID]); // 1 = "on", 0 = "off" + Serial.print (" "); + } + Serial.println(); + } + + + // when using the single-threshold power diversion algorithm, a counter needs + // to be reset whenever the energy level in the accumulator crosses the mid-point + // + enum energyStates energyStateOnLastLoop = energyStateNow; + + if (energyInBucket_long > midPointOfEnergyBucket_long) { + energyStateNow = UPPER_HALF; } + else { + energyStateNow = LOWER_HALF; } + + if (energyStateNow != energyStateOnLastLoop) { + mainsCyclesSinceLastMidPointCrossing = 0; } + + + // clear the per-cycle accumulators for use in this new mains cycle. + samplesDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + if (triggerNeedsToBeArmed == true) + { + // check to see whether the trigger device can now be reliably armed + if(samplesDuringThisMainsCycle == 3) // should always exceed 20V (the min for trigger) + { + /* Now it's time to determine whether any of the the loads need to be changed. + * This is a 2-stage process: + * First, change the LOGICAL loads as necessary, then update the PHYSICAL + * loads according to the mapping that exists between them. The mapping is + * 1:1 by default but can be altered by a hardware switch which allows the + * priority of the remote load to be altered. + * This code uses a single-threshold algorithm which relies on regular switching + * of the load. + */ + if (mainsCyclesSinceLastMidPointCrossing > postMidPointCrossingDelay_cycles) + { + if (energyInBucket_long > midPointOfEnergyBucket_long) + { + increaseLoadIfPossible(); // to reduce the level in the bucket + } + else + { + decreaseLoadIfPossible(); // to increase the level in the bucket + } + } + + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + // update all the physical loads + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger + digitalWrite(physicalLoad_1_pin, physicalLoadState[1]); // active low for trigger + digitalWrite(physicalLoad_2_pin, physicalLoadState[2]); // active low for trigger + digitalWrite(physicalLoad_3_pin, physicalLoadState[3]); // active low for trigger + digitalWrite(physicalLoad_4_pin, physicalLoadState[4]); // active low for trigger + digitalWrite(physicalLoad_5_pin, physicalLoadState[5]); // active low for trigger + + // update the Energy Diversion Detector + if (physicalLoadState[0] == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + + // clear the flag which ensures that loads are only updated once per mains cycle + triggerNeedsToBeArmed = false; + } + } + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > startUpPeriod * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + samplesDuringThisMainsCycle = 0; + Serial.println ("Go!"); + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + +// checkOutputModeSelection(); // updates outputMode if switch is changed + checkLoadPrioritySelection(); // updates load priorities if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is positive + + // processing for EVERY pair of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform +// long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform +// long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + samplesDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityOfLastSampleV = polarityNow; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + + +void increaseLoadIfPossible() +{ + /* if permitted by A/F rules, turn on the highest priority logical load that is not already on. + */ + boolean changed = false; + + // Only one load may operate freely at a time. Other loads are prevented from + // switching until a sufficient period has elapsed since the last transition. + // This scheme allows a lower priority load to contribute if a higher priority + // load is not having the desired effect, but not immediately. + // + if (energyInBucket_long >= energyAtLastOnTransition_long) + { +// Serial.print('+'); // useful for testing this logic + boolean timeout = (mainsCyclesSinceLastChangeOfLoadState > interLoadSeparationDelay_cycles); + for (int i = 0; i < noOfDumploads && !changed; i++) + { + if (logicalLoadState[i] == LOAD_OFF) + { + if ((i == activeLoadID) || timeout) + { + logicalLoadState[i] = LOAD_ON; + mainsCyclesSinceLastChangeOfLoadState = 0; + energyAtLastOnTransition_long = energyInBucket_long; + energyAtLastOffTransition_long = midPointOfEnergyBucket_long; // reset the 'opposite' mechanism. + activeLoadID = i; + changed = true; + } + } + } + } + else + { + // energy level has not risen so there's no need to apply any more load + } +// return (changed); +} + +void decreaseLoadIfPossible() +{ + /* if permitted by A/F rules, turn off the lowest priority logical load that is not already off. + */ + boolean changed = false; + + // Only one load may operate freely at a time. Other loads are prevented from + // switching until a sufficient period has elapsed since the last transition. + // This scheme allows a lower priority load to contribute if a higher priority + // load is not having the desired effect, but not immediately. + // + if (energyInBucket_long <= energyAtLastOffTransition_long) + { +// Serial.print('-'); // useful for testing this logic + boolean timeout = (mainsCyclesSinceLastChangeOfLoadState > interLoadSeparationDelay_cycles); +// for (int i = 0; i < noOfDumploads && !done; i++) + for (int i = (noOfDumploads -1); i >= 0 && !changed; i--) + { + if (logicalLoadState[i] == LOAD_ON) + { + if ((i == activeLoadID) || timeout) + { + logicalLoadState[i] = LOAD_OFF; + mainsCyclesSinceLastChangeOfLoadState = 0; + energyAtLastOffTransition_long = energyInBucket_long; + energyAtLastOnTransition_long = midPointOfEnergyBucket_long; // reset the 'opposite' mechanism. + activeLoadID = i; + changed = true; + } + } + } + } + else + { + // energy level has not fallen so there's no need to remove any more load + } +// return (changed); +} + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * By default, the association between the physical and logical loads is 1:1. If + * the physical load 1 is set to have priority, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * For this implementation, all loads are 'local' because the RF facility is not in use. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } + + if (loadPriorityMode == LOAD_1_HAS_PRIORITY) + { + // swap physical loads 0 & 1 if remote load has priority + physicalLoadState[0] = logicalLoadState[1]; + physicalLoadState[1] = logicalLoadState[0]; + } +} + + +// this function changes the value of the load priorities if the state of the external switch is altered +void checkLoadPrioritySelection() +{ + static byte loadPrioritySwitchCcount = 0; + int pinState = digitalRead(loadPrioritySelectorPin); + if (pinState != loadPriorityMode) + { + loadPrioritySwitchCcount++; + } + if (loadPrioritySwitchCcount >= 20) + { + loadPrioritySwitchCcount = 0; + loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable + Serial.print ("loadPriority selection changed to "); + if (loadPriorityMode == LOAD_0_HAS_PRIORITY) { + Serial.println ( "load 0"); } + else { + Serial.println ( "load 1"); } + } +} + + +// Although this sketch always operates in ANTI_FLICKER mode, it was convenient +// to leave this mechanism in place. +// +void configureParamsForSelectedOutputMode() +{ + + if (outputMode == ANTI_FLICKER) + { + postMidPointCrossingDelay_cycles = postMidPointCrossingDelayForAF_cycles; + } + else + { + postMidPointCrossingDelay_cycles = 0; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" midPointOfEnergyBucket_long = "); + Serial.println(midPointOfEnergyBucket_long); + Serial.print(" postMidPointCrossingDelay_cycles = "); + Serial.println(postMidPointCrossingDelay_cycles); + Serial.print(" interLoadSeparationDelay_cycles = "); + Serial.println(interLoadSeparationDelay_cycles); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +// valueToBeDisplayed++; +} + + + + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_CAT5_4.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_CAT5_4.ino new file mode 100644 index 0000000..fa8399a --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_CAT5_4.ino @@ -0,0 +1,1237 @@ +/* Mk2_multiLoad_CAT5_4.ino + * + * This sketch is for diverting surplus PV power using multiple hard-wired loads. + * An external switch allows either load 0 or Load 1 to have the highest + * priority. Any number of loads can be supported by the logic, a dedicated + * IO pin being required for each one. + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The selector switch, as mentioned above, connects to the "mode" port. For this + * version of the Mk2 code, the system uses a single-threshold version of the + * anti-flicker algorithm which is well suited for multiple loads. As the 'normal' + * mode is no longer required, the 'mode' port has been re-assigned for priority + * selection. + * + * The integral voltage sensor is fed from one of the secondary coils of the transformer. + * Current is measured via Current Transformers at the CT1 and CT2 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the 'diverted' current, so that energy which is diverted via the + * local dump load can be recorded and displayed locally. + * + * A persistence-based 4-digit display is supported. To free up the necessary IO pins + * for driving multiple loads, the pin-saving hardware needs to be in place. + * These extra logic chips (ICs 3 and 4) reduce the number of IO pins + * that are needed to drive the display. The freed-up pins are available at the + * J1-5 connector. The uppermost position has been assigned to drive Load 1, + * the next one down is for Load 2, and the lowest one is for Load 5. The control signal + * for Load 0 is available at the "trigger" connector. + * + * With the green (rev 2.1) version of my PCB, each of the additional outputs has an + * associated ground pin. It is therefore more sensible for those outputs to be active-high + * rather than active-low. With a 5V regulator rather than the normal 3.3V one, these + * outputs are able to drive an SSR directly. Unlike a triac, an SSR can be CE-marked which + * may be an important feature for some applications. + * + * This sketch has many similarities with Revision 5 of the Mk2i PV Router code that I + * have posted on the OpenEnergyMonitor forum. That version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * + * September 2014: renamed as Mk2_multiLoad_CAT5_3, with these changes: + * - reimplementation of cycleCount, as it could have overflowed with unpredictable results; + * - the functions increaseLoadIfPossible() and decreaseLoadIfPossible() have been tidied; + * - energyThreshold_long has been renamed as midPointOfEnergyBucket_long. + * + * December 2014: renamed as Mk2_multiLoad_CAT5_4, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed); + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances; + * - the logic for each of the 5 additional loads has been inverted by use of the '!' character. + * these outputs are now active-high rather than active-low + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define SWEETZONE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +const byte noOfDumploads = 6; // The logic expects a minimum of 2 dumploads, + // for local & remote loads, but neither has to + // be physically present. + +// definitions of enumerated types and instances of same +enum polarities {NEGATIVE, POSITIVE}; +enum outputModes {ANTI_FLICKER, NORMAL}; +enum loadPriorityModes {LOAD_1_HAS_PRIORITY, LOAD_0_HAS_PRIORITY}; + +enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are active low +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +enum energyStates {LOWER_HALF, UPPER_HALF}; // for single threshold AF algorithm +enum energyStates energyStateNow; + +// ---------------- Extra Features selection ---------------------- +// +// - WORKLOAD_CHECK, for determining how much spare processing time there is. +// +// #define WORKLOAD_CHECK // <-- Include this line is this feature is required + + +// For most single-load Mk2 systems, the power-diversion logic can operate in either of two modes: +// +// - NORMAL, where the triac switches rapidly on/off to maintain a constant energy level. +// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations +// of the local mains voltage. +// +// For this multi-load version, the same mechanism has been retained but the +// output mode is hard-coded as below: +enum outputModes outputMode = ANTI_FLICKER; + +// In this multi-load version, the external switch is re-used to determine the load priority +enum loadPriorityModes loadPriorityMode = LOAD_0_HAS_PRIORITY; + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +const byte physicalLoad_2_pin = 2; // <-- to control an additional load +const byte loadPrioritySelectorPin = 3; // <-- this is the "mode" port +const byte physicalLoad_0_pin = 4; // <-- this is the "trigger" port +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +const byte physicalLoad_3_pin = 10; // <-- to control an additional load +const byte physicalLoad_5_pin = 11; // <-- to control an additional load +const byte physicalLoad_1_pin = 12; // <-- to control an additional load +const byte physicalLoad_4_pin = 13; // <-- to control an additional load + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + + +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +long energyInBucket_long = 0; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long midPointOfEnergyBucket_long; // used for 'normal' and single-threshold 'AF' logic +// int phaseCal_grid_int; // to avoid the need for floating-point maths +// int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; + +int postMidPointCrossingDelay_cycles; // assigned in setup(), different for each output mode +const int postMidPointCrossingDelayForAF_cycles = 25; // in 20 ms counts +const int interLoadSeparationDelay_cycles = 25; // in 20 ms cycle counts (for both output modes) +byte activeLoadID; // only one load may operate freely at a time. + +long energyAtLastOffTransition_long; +long energyAtLastOnTransition_long; +int mainsCyclesSinceLastMidPointCrossing = 0; +int mainsCyclesSinceLastChangeOfLoadState = 0; + + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +int sampleI_grid; +int sampleI_diverted; +int sampleV; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define POLARITY_CHECK_MAXCOUNT 2 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// With most hardware, the default values are likely to work fine without +// need for change. A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.044; // for CT2 + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When +// measuring a resistive load, the voltage and current waveforms should be perfectly +// aligned. In this situation, the Power Factor will be 1. +// +// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct +// value of phaseCal for any hardware configuration. An index of my various Mk2-related +// exhibits is available at http://openenergymonitor.org/emon/node/1757 +// +//const float phaseCal_grid = 1.0; <--- not used in this version +//const float phaseCal_diverted = 1.0; <--- not used in this version + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 10 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // Energy Diversion Detection (for the local load only) +long requiredExportPerMainsCycle_inIEU; +float IEUtoJoulesConversion_CT1; + +void setup() +{ + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for the local dump-load + pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_2_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_3_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_4_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_5_pin, OUTPUT); // driver pin for an additional load + + for(int i = 0; i< noOfDumploads; i++) + { + logicalLoadState[i] = LOAD_OFF; + physicalLoadState[i] = LOAD_OFF; + } + + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // the local load is active low. + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // additional loads are active high. + digitalWrite(physicalLoad_2_pin, !physicalLoadState[2]); // additional loads are active high + digitalWrite(physicalLoad_3_pin, !physicalLoadState[3]); // additional loads are active high + digitalWrite(physicalLoad_4_pin, !physicalLoadState[4]); // additional loads are active high + digitalWrite(physicalLoad_5_pin, !physicalLoadState[5]); // additional loads are active high + + pinMode(loadPrioritySelectorPin, INPUT); // this pin is tracked to the "mode" connector + digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and + loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority + + delay(5000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_multiLoad_CAT5_4.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, calibration values that are supplied in floating point + // form need to be rescaled. + // +// phaseCal_grid_int = phaseCal_grid * 256; // for integer maths <-- not supported +// phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths <-- not supported + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); +// energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up + midPointOfEnergyBucket_long = capacityOfEnergyBucket_long / 2; + energyAtLastOffTransition_long = midPointOfEnergyBucket_long; + energyAtLastOnTransition_long = midPointOfEnergyBucket_long; + + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the correct scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples. The main processor and +// the ADC work autonomously, their operation being only linked via the dataReady flag. +// As soon as a new set of data is made available by the ADC, the main processor can +// start to work on it immediately. +// +void allGeneralProcessing() +{ + static boolean beyondStartUpPhase = false; // start-up delay, allows things to settle + static boolean triggerNeedsToBeArmed = false; // once per mains cycle (+ve half) + static long sumP_grid; // for per-cycle summation of 'real power' + static long sumP_diverted; // for per-cycle summation of 'real power' + static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) + static long lastSampleVminusDC_long; // for the phaseCal algorithm + static byte perSecondCounter = 0; + + // remove DC offset from the raw voltage sample by subtracting the accurate value + // as determined by a LP filter. + long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long; + + // determine the polarity of the latest voltage sample + if(sampleVminusDC_long > 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + // cycleCount++; <- this mechanism is unsafe, it will eventually overflow + mainsCyclesSinceLastMidPointCrossing++; + mainsCyclesSinceLastChangeOfLoadState++; + + // a simple routine for checking the continuity of the sampling scheme + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } + + triggerNeedsToBeArmed = true; // the trigger is armed once during each +ve half-cycle + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / sampleSetsDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- for non-standard use + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + if (EDD_isActive) // Energy Diversion Display + { + // When locally diverted energy is being monitored, the latest contribution + // needs to be added to an accumulator which operates with maximum precision. + // + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) { + realEnergy_diverted = 0; } // to avoid 'creep' + + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + perSecondCounter++; + if(perSecondCounter >= CYCLES_PER_SECOND) + { + perSecondCounter = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); // occurs every second +/* + Serial.print (energyInBucket_long * IEUtoJoulesConversion_CT1); + Serial.print (", "); + for (byte loadID = 0; loadID < noOfDumploads; loadID++) + { + Serial.print (!physicalLoadState[loadID]); // 1 = "on", 0 = "off" + Serial.print (" "); + } + Serial.println(); +*/ + } + + + // when using the single-threshold power diversion algorithm, a counter needs + // to be reset whenever the energy level in the accumulator crosses the mid-point + // + enum energyStates energyStateOnLastLoop = energyStateNow; + + if (energyInBucket_long > midPointOfEnergyBucket_long) { + energyStateNow = UPPER_HALF; } + else { + energyStateNow = LOWER_HALF; } + + if (energyStateNow != energyStateOnLastLoop) { + mainsCyclesSinceLastMidPointCrossing = 0; } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + if (triggerNeedsToBeArmed == true) + { + // check to see whether the trigger device can now be reliably armed + if(sampleSetsDuringThisMainsCycle == 3) // should always exceed 20V (the min for trigger) + { + /* Now it's time to determine whether any of the the loads need to be changed. + * This is a 2-stage process: + * First, change the LOGICAL loads as necessary, then update the PHYSICAL + * loads according to the mapping that exists between them. The mapping is + * 1:1 by default but can be altered by a hardware switch which allows the + * priority of the remote load to be altered. + * This code uses a single-threshold algorithm which relies on regular switching + * of the load. + */ + if (mainsCyclesSinceLastMidPointCrossing > postMidPointCrossingDelay_cycles) + { + if (energyInBucket_long > midPointOfEnergyBucket_long) + { + increaseLoadIfPossible(); // to reduce the level in the bucket + } + else + { + decreaseLoadIfPossible(); // to increase the level in the bucket + } + } + + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + // update all the physical loads + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // active high for additional load + digitalWrite(physicalLoad_2_pin, !physicalLoadState[2]); // active high for additional load + digitalWrite(physicalLoad_3_pin, !physicalLoadState[3]); // active high for additional load + digitalWrite(physicalLoad_4_pin, !physicalLoadState[4]); // active high for additional load + digitalWrite(physicalLoad_5_pin, !physicalLoadState[5]); // active high for additional load + + // update the Energy Diversion Detector + if (physicalLoadState[0] == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + + // clear the flag which ensures that loads are only updated once per mains cycle + triggerNeedsToBeArmed = false; + } + } + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > startUpPeriod * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringThisMainsCycle = 0; + sampleCount_forContinuityChecker = 0; + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + +// checkOutputModeSelection(); // updates outputMode if switch is changed + checkLoadPrioritySelection(); // updates load priorities if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is positive + + // processing for EVERY pair of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform +// long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform +// long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + sampleSetsDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count >= POLARITY_CHECK_MAXCOUNT) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + + +void increaseLoadIfPossible() +{ + /* if permitted by A/F rules, turn on the highest priority logical load that is not already on. + */ + boolean changed = false; + + // Only one load may operate freely at a time. Other loads are prevented from + // switching until a sufficient period has elapsed since the last transition. + // This scheme allows a lower priority load to contribute if a higher priority + // load is not having the desired effect, but not immediately. + // + if (energyInBucket_long >= energyAtLastOnTransition_long) + { +// Serial.print('+'); // useful for testing this logic + boolean timeout = (mainsCyclesSinceLastChangeOfLoadState > interLoadSeparationDelay_cycles); + for (int i = 0; i < noOfDumploads && !changed; i++) + { + if (logicalLoadState[i] == LOAD_OFF) + { + if ((i == activeLoadID) || timeout) + { + logicalLoadState[i] = LOAD_ON; + mainsCyclesSinceLastChangeOfLoadState = 0; + energyAtLastOnTransition_long = energyInBucket_long; + energyAtLastOffTransition_long = midPointOfEnergyBucket_long; // reset the 'opposite' mechanism. + activeLoadID = i; + changed = true; + } + } + } + } + else + { + // energy level has not risen so there's no need to apply any more load + } +// return (changed); +} + +void decreaseLoadIfPossible() +{ + /* if permitted by A/F rules, turn off the lowest priority logical load that is not already off. + */ + boolean changed = false; + + // Only one load may operate freely at a time. Other loads are prevented from + // switching until a sufficient period has elapsed since the last transition. + // This scheme allows a lower priority load to contribute if a higher priority + // load is not having the desired effect, but not immediately. + // + if (energyInBucket_long <= energyAtLastOffTransition_long) + { +// Serial.print('-'); // useful for testing this logic + boolean timeout = (mainsCyclesSinceLastChangeOfLoadState > interLoadSeparationDelay_cycles); +// for (int i = 0; i < noOfDumploads && !done; i++) + for (int i = (noOfDumploads -1); i >= 0 && !changed; i--) + { + if (logicalLoadState[i] == LOAD_ON) + { + if ((i == activeLoadID) || timeout) + { + logicalLoadState[i] = LOAD_OFF; + mainsCyclesSinceLastChangeOfLoadState = 0; + energyAtLastOffTransition_long = energyInBucket_long; + energyAtLastOnTransition_long = midPointOfEnergyBucket_long; // reset the 'opposite' mechanism. + activeLoadID = i; + changed = true; + } + } + } + } + else + { + // energy level has not fallen so there's no need to remove any more load + } +// return (changed); +} + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * By default, the association between the physical and logical loads is 1:1. If + * the physical load 1 is set to have priority, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * For this implementation, all loads are 'local' because the RF facility is not in use. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } + + if (loadPriorityMode == LOAD_1_HAS_PRIORITY) + { + // swap physical loads 0 & 1 if remote load has priority + physicalLoadState[0] = logicalLoadState[1]; + physicalLoadState[1] = logicalLoadState[0]; + } +} + + +// this function changes the value of the load priorities if the state of the external switch is altered +void checkLoadPrioritySelection() +{ + static byte loadPrioritySwitchCcount = 0; + int pinState = digitalRead(loadPrioritySelectorPin); + if (pinState != loadPriorityMode) + { + loadPrioritySwitchCcount++; + } + if (loadPrioritySwitchCcount >= 20) + { + loadPrioritySwitchCcount = 0; + loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable + Serial.print ("loadPriority selection changed to "); + if (loadPriorityMode == LOAD_0_HAS_PRIORITY) { + Serial.println ( "load 0"); } + else { + Serial.println ( "load 1"); } + } +} + + +// Although this sketch always operates in ANTI_FLICKER mode, it was convenient +// to leave this mechanism in place. +// +void configureParamsForSelectedOutputMode() +{ + + if (outputMode == ANTI_FLICKER) + { + postMidPointCrossingDelay_cycles = postMidPointCrossingDelayForAF_cycles; + } + else + { + postMidPointCrossingDelay_cycles = 0; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" midPointOfEnergyBucket_long = "); + Serial.println(midPointOfEnergyBucket_long); + Serial.print(" postMidPointCrossingDelay_cycles = "); + Serial.println(postMidPointCrossingDelay_cycles); + Serial.print(" interLoadSeparationDelay_cycles = "); + Serial.println(interLoadSeparationDelay_cycles); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +// valueToBeDisplayed++; +} + + + + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_wired_5.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_wired_5.ino new file mode 100644 index 0000000..9d2d8ce --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_wired_5.ino @@ -0,0 +1,1226 @@ +/* Mk2_multiLoad_wired_5.ino is based on Mk2_multiLoad_CAT5_4.ino + * + * This sketch is for diverting surplus PV power using multiple hard-wired loads. + * An external switch allows either load 0 or Load 1 to have the highest + * priority. Any number of loads can be supported by the logic, a dedicated + * IO pin being required for each one. + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The selector switch, as mentioned above, connects to the "mode" port which has been + * re-assigned for priority selection. "Normal" mode can be achieved by setting the + * anti-flicker offset prameter to zero at compile-time. + * + * The integral voltage sensor is fed from one of the secondary coils of the transformer. + * Current is measured via Current Transformers at the CT1 and CT2 ports. + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the 'diverted' current, so that energy which is diverted via the primary + * dump-load can be recorded and displayed locally. + * + * A persistence-based 4-digit display is supported. To free up the necessary IO pins + * for driving multiple loads, the pin-saving hardware needs to be in place. + * These extra logic chips (ICs 3 and 4) reduce the number of IO pins + * that are needed to drive the display. The freed-up pins are available at the + * J1-5 connector. The uppermost position has been assigned to drive Load 1, + * the next one down is for Load 2, and the lowest one is for Load 5. The control signal + * for Load 0 is available at the "trigger" connector. + * + * With the green (rev 2.1) version of my PCB, each of the additional outputs has an + * associated ground pin. It is therefore more sensible for those outputs to be active-high + * rather than active-low. With a 5V regulator rather than the normal 3.3V one, these + * outputs are able to drive an SSR directly. energymonitor.org/emon/node/1757 + * + * September 2014: renamed as Mk2_multiLoad_CAT5_3, with these changes: + * - reimplementation of cycleCount, as it could have overflowed with unpredictable results; + * - the functions increaseLoadIfPossible() and decreaseLoadIfPossible() have been tidied; + * - energyThreshold_long has been renamed as midPointOfEnergyBucket_long. + * + * December 2014: renamed as Mk2_multiLoad_CAT5_4, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed); + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances; + * - the logic for each of the 5 additional loads has been inverted by use of the '!' character. + * these outputs are now active-high rather than active-low + * + * November 2015: renamed as Mk2_multiLoad_wired_5, with these changes: + * - the original twin-threshold algorithm for energy state management has been reinstated; + * - improved mechanism for controlling multiple loads (faster and more accurate); + * - the phaseCal mechanism has been reinstated; + * - SWEETZONE_IN_JOULES has been replaced by WORKING_RANGE_IN_JOULES. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define WORKING_RANGE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +const byte noOfDumploads = 6; // The logic expects a minimum of 2 dumploads, + // for local & remote loads, but neither has to + // be physically present. + +// definitions of enumerated types and instances of same +enum polarities {NEGATIVE, POSITIVE}; +enum outputModes {ANTI_FLICKER, NORMAL}; +enum loadPriorityModes {LOAD_1_HAS_PRIORITY, LOAD_0_HAS_PRIORITY}; + +enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are active low +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +// ---------------- Extra Features selection ---------------------- +// +// - WORKLOAD_CHECK, for determining how much spare processing time there is. +// +// #define WORKLOAD_CHECK // <-- Include this line is this feature is required + + +// For most single-load Mk2 systems, the power-diversion logic can operate in either of two modes: +// +// - NORMAL, where the triac switches rapidly on/off to maintain a constant energy level. +// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations +// of the local mains voltage. +// +// For this multi-load version, the same mechanism has been retained but the +// output mode is hard-coded as below: +enum outputModes outputMode = ANTI_FLICKER; + +// In this multi-load version, the external switch is re-used to determine the load priority +enum loadPriorityModes loadPriorityMode = LOAD_0_HAS_PRIORITY; + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +const byte physicalLoad_2_pin = 2; // <-- to control an additional load +const byte loadPrioritySelectorPin = 3; // <-- this is the "mode" port +const byte physicalLoad_0_pin = 4; // <-- this is the "trigger" port +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +const byte physicalLoad_3_pin = 10; // <-- to control an additional load +const byte physicalLoad_5_pin = 11; // <-- to control an additional load +const byte physicalLoad_1_pin = 12; // <-- to control an additional load +const byte physicalLoad_4_pin = 13; // <-- to control an additional load + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + + +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +long energyInBucket_long = 0; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long midPointOfEnergyBucket_long; // used for 'normal' and single-threshold 'AF' logic +int phaseCal_grid_int; // to avoid the need for floating-point maths +int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; + +long lowerThreshold_default; +long lowerEnergyThreshold; +long upperThreshold_default; +long upperEnergyThreshold; + +boolean recentTransition; +byte postTransitionCount; +#define POST_TRANSITION_MAX_COUNT 3 // <-- allows each transition to take effect +byte activeLoad = 0; + +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.4 + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +int sampleI_grid; +int sampleI_diverted; +int sampleV; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define POLARITY_CHECK_MAXCOUNT 2 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// With most hardware, the default values are likely to work fine without +// need for change. A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.044; // for CT2 + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When +// measuring a resistive load, the voltage and current waveforms should be perfectly +// aligned. In this situation, the Power Factor will be 1. +// +// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct +// value of phaseCal for any hardware configuration. An index of my various Mk2-related +// exhibits is available at http://openenergymonitor.org/emon/node/1757 +// +const float phaseCal_grid = 1.0; // default value +const float phaseCal_diverted = 1.0; // default value + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 10 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // Energy Diversion Detection (for the local load only) +long requiredExportPerMainsCycle_inIEU; +float IEUtoJoulesConversion_CT1; + +void setup() +{ + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for the local dump-load + pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_2_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_3_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_4_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_5_pin, OUTPUT); // driver pin for an additional load + + for(int i = 0; i< noOfDumploads; i++) + { + logicalLoadState[i] = LOAD_OFF; + physicalLoadState[i] = LOAD_OFF; + } + + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // the local load is active low. + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // additional loads are active high. + digitalWrite(physicalLoad_2_pin, !physicalLoadState[2]); // additional loads are active high + digitalWrite(physicalLoad_3_pin, !physicalLoadState[3]); // additional loads are active high + digitalWrite(physicalLoad_4_pin, !physicalLoadState[4]); // additional loads are active high + digitalWrite(physicalLoad_5_pin, !physicalLoadState[5]); // additional loads are active high + + pinMode(loadPrioritySelectorPin, INPUT); // this pin is tracked to the "mode" connector + digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and + loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority + + delay(5000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_multiLoad_wired_5.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, calibration values that are supplied in floating point + // form need to be rescaled. + // + phaseCal_grid_int = phaseCal_grid * 256; // for integer maths + phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); +// energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up + midPointOfEnergyBucket_long = capacityOfEnergyBucket_long / 2; + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the correct scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples. The main processor and +// the ADC work autonomously, their operation being only linked via the dataReady flag. +// As soon as a new set of data is made available by the ADC, the main processor can +// start to work on it immediately. +// +void allGeneralProcessing() +{ + static boolean beyondStartUpPhase = false; // start-up delay, allows things to settle + static long sumP_grid; // for per-cycle summation of 'real power' + static long sumP_diverted; // for per-cycle summation of 'real power' + static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) + static long lastSampleVminusDC_long; // for the phaseCal algorithm + static byte perSecondCounter = 0; + + // remove DC offset from the raw voltage sample by subtracting the accurate value + // as determined by a LP filter. + long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long; + + // determine the polarity of the latest voltage sample + if(sampleVminusDC_long > 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + // cycleCount++; <- this mechanism is unsafe, it will eventually overflow + + // a simple routine for checking the continuity of the sampling scheme + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / sampleSetsDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- for non-standard use + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Applying max and min limits to bucket's level has been + // deferred until after energy-related decisions have been taken. + + if (EDD_isActive) // Energy Diversion Display + { + // When locally diverted energy is being monitored, the latest contribution + // needs to be added to an accumulator which operates with maximum precision. + // + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) { + realEnergy_diverted = 0; } // to avoid 'creep' + + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + perSecondCounter++; + if(perSecondCounter >= CYCLES_PER_SECOND) + { + perSecondCounter = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); // occurs every second +/* + Serial.print (energyInBucket_long * IEUtoJoulesConversion_CT1); + Serial.print (", "); + for (byte loadID = 0; loadID < noOfDumploads; loadID++) + { + Serial.print (!physicalLoadState[loadID]); // 1 = "on", 0 = "off" + Serial.print (" "); + } + Serial.println(); +*/ +// Serial.println(activeLoad); + } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + // check to see whether the trigger device can now be reliably armed + if(sampleSetsDuringThisMainsCycle == 3) // should always exceed 20V (the min for trigger) + { + /* Determining whether any of the loads need to be changed is is a 3-stage process: + * - change the LOGICAL load states as necessary to maintain the energy level + * - update the PHYSICAL load states according to the logical -> physical mapping + * - update the driver lines for each of the loads. + */ + + // Restrictions apply for the period immediately after a load has been switched. + // Here the recentTransition flag is checked and updated as necessary. + if (recentTransition) + { + postTransitionCount++; + if (postTransitionCount >= POST_TRANSITION_MAX_COUNT) + { + recentTransition = false; + } + } + + if (energyInBucket_long > midPointOfEnergyBucket_long) + { + // the energy state is in the upper half of the working range + lowerEnergyThreshold = lowerThreshold_default; // reset the "opposite" threshold + if (energyInBucket_long > upperEnergyThreshold) + { + // Because the energy level is high, some action may be required + boolean OK_toAddLoad = true; + byte tempLoad = nextLogicalLoadToBeAdded(); + if (tempLoad < noOfDumploads) + { + // a load which is now OFF has been identified for potentially being switched ON + if (recentTransition) + { + // During the post-transition period, any increase in the energy level is noted. + upperEnergyThreshold = energyInBucket_long; + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toAddLoad = false; + } + } + + if (OK_toAddLoad) + { + logicalLoadState[tempLoad] = LOAD_ON; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + } + } + } + } + else + { // the energy state is in the lower half of the working range + upperEnergyThreshold = upperThreshold_default; // reset the "opposite" threshold + if (energyInBucket_long < lowerEnergyThreshold) + { + // Because the energy level is low, some action may be required + boolean OK_toRemoveLoad = true; + byte tempLoad = nextLogicalLoadToBeRemoved(); + if (tempLoad < noOfDumploads) + { + // a load which is now ON has been identified for potentially being switched OFF + if (recentTransition) + { + // During the post-transition period, any increase in the energy level is noted. + lowerEnergyThreshold = energyInBucket_long; + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toRemoveLoad = false; + } + } + + if (OK_toRemoveLoad) + { + logicalLoadState[tempLoad] = LOAD_OFF; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + } + } + } + } + + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + // update each of the physical loads + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // active high for additional load + digitalWrite(physicalLoad_2_pin, !physicalLoadState[2]); // active high for additional load + digitalWrite(physicalLoad_3_pin, !physicalLoadState[3]); // active high for additional load + digitalWrite(physicalLoad_4_pin, !physicalLoadState[4]); // active high for additional load + digitalWrite(physicalLoad_5_pin, !physicalLoadState[5]); // active high for additional load + + // update the Energy Diversion Detector + if (physicalLoadState[0] == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + } + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > startUpPeriod * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringThisMainsCycle = 0; + sampleCount_forContinuityChecker = 0; + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + +// checkOutputModeSelection(); // updates outputMode if switch is changed + checkLoadPrioritySelection(); // updates load priorities if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is positive + + // processing for EVERY pair of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform + long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); +// long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform + long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); +// long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + sampleSetsDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + + refreshDisplay(); +} + + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count >= POLARITY_CHECK_MAXCOUNT) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + + +byte nextLogicalLoadToBeAdded() +{ + byte retVal = noOfDumploads; + boolean success = false; + for (byte index = 0; index < noOfDumploads && !success; index++) + { + if (logicalLoadState[index] == LOAD_OFF) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +byte nextLogicalLoadToBeRemoved() +{ + byte retVal = noOfDumploads; + boolean success = false; + for (byte index = (noOfDumploads -1); index >= 0 && !success; index--) + { + if (logicalLoadState[index] == LOAD_ON) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * The association between the physical and logical loads is 1:1. By default, numerical + * equivalence is maintained, so logical(N) maps to physical(N). If physical load 1 is set + * to have priority, rather than physical load 0, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * Any other mapping relaionships could be configured here. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } + + if (loadPriorityMode == LOAD_1_HAS_PRIORITY) + { + // swap physical loads 0 & 1 if remote load has priority + physicalLoadState[0] = logicalLoadState[1]; + physicalLoadState[1] = logicalLoadState[0]; + } +} + + +// this function changes the value of the load priorities if the state of the external switch is altered +void checkLoadPrioritySelection() +{ + static byte loadPrioritySwitchCount = 0; + int pinState = digitalRead(loadPrioritySelectorPin); + if (pinState != loadPriorityMode) + { + loadPrioritySwitchCount++; + } + if (loadPrioritySwitchCount >= 20) + { + loadPrioritySwitchCount = 0; + loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable + Serial.print ("loadPriority selection changed to "); + if (loadPriorityMode == LOAD_0_HAS_PRIORITY) { + Serial.println ( "load 0"); } + else { + Serial.println ( "load 1"); } + } +} + + +// Although this sketch always operates in ANTI_FLICKER mode, it was convenient +// to leave this mechanism in place. +// +void configureParamsForSelectedOutputMode() +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerThreshold_default = + capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperThreshold_default = + capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerThreshold_default = capacityOfEnergyBucket_long * 0.5; + upperThreshold_default = capacityOfEnergyBucket_long * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" lowerEnergyThreshold = "); + Serial.println(lowerThreshold_default); + Serial.print(" upperEnergyThreshold = "); + Serial.println(upperThreshold_default); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +// valueToBeDisplayed++; +} + + + + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_wired_5a.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_wired_5a.ino new file mode 100644 index 0000000..1d05d49 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_wired_5a.ino @@ -0,0 +1,1234 @@ +/* Mk2_multiLoad_wired_5a.ino + * is based on Mk2_multiLoad_wired_5.ino + * which is based on Mk2_multiLoad_CAT5_4.ino + * + * This sketch is for diverting surplus PV power using multiple hard-wired loads. + * An external switch allows either load 0 or Load 1 to have the highest + * priority. Any number of loads can be supported by the logic, a dedicated + * IO pin being required for each one. + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The selector switch, as mentioned above, connects to the "mode" port which has been + * re-assigned for priority selection. "Normal" mode can be achieved by setting the + * anti-flicker offset prameter to zero at compile-time. + * + * The integral voltage sensor is fed from one of the secondary coils of the transformer. + * Current is measured via Current Transformers at the CT1 and CT2 ports. + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the 'diverted' current, so that energy which is diverted via the primary + * dump-load can be recorded and displayed locally. + * + * A persistence-based 4-digit display is supported. To free up the necessary IO pins + * for driving multiple loads, the pin-saving hardware needs to be in place. + * These extra logic chips (ICs 3 and 4) reduce the number of IO pins + * that are needed to drive the display. The freed-up pins are available at the + * J1-5 connector. The uppermost position has been assigned to drive Load 1, + * the next one down is for Load 2, and the lowest one is for Load 5. The control signal + * for Load 0 is available at the "trigger" connector. + * + * With the green (rev 2.1) version of my PCB, each of the additional outputs has an + * associated ground pin. It is therefore more sensible for those outputs to be active-high + * rather than active-low. With a 5V regulator rather than the normal 3.3V one, these + * outputs are able to drive an SSR directly. energymonitor.org/emon/node/1757 + * + * September 2014: renamed as Mk2_multiLoad_CAT5_3, with these changes: + * - reimplementation of cycleCount, as it could have overflowed with unpredictable results; + * - the functions increaseLoadIfPossible() and decreaseLoadIfPossible() have been tidied; + * - energyThreshold_long has been renamed as midPointOfEnergyBucket_long. + * + * December 2014: renamed as Mk2_multiLoad_CAT5_4, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed); + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances; + * - the logic for each of the 5 additional loads has been inverted by use of the '!' character. + * these outputs are now active-high rather than active-low + * + * November 2015: renamed as Mk2_multiLoad_wired_5, with these changes: + * - the original twin-threshold algorithm for energy state management has been reinstated; + * - improved mechanism for controlling multiple loads (faster and more accurate); + * - the phaseCal mechanism has been reinstated; + * - SWEETZONE_IN_JOULES has been replaced by WORKING_RANGE_IN_JOULES. + * + * January 2015: renamed as Mk2_multiLoad_wired_5a, with these changes: + * - minor bug-fix in allGeneralProcessing() which affects how the energy thresholds are adjusted immediately + * after a change of load-state has tqaken place. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define WORKING_RANGE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +const byte noOfDumploads = 6; // The logic expects a minimum of 2 dumploads, + // for local & remote loads, but neither has to + // be physically present. + +// definitions of enumerated types and instances of same +enum polarities {NEGATIVE, POSITIVE}; +enum outputModes {ANTI_FLICKER, NORMAL}; +enum loadPriorityModes {LOAD_1_HAS_PRIORITY, LOAD_0_HAS_PRIORITY}; + +enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are active low +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +// ---------------- Extra Features selection ---------------------- +// +// - WORKLOAD_CHECK, for determining how much spare processing time there is. +// +// #define WORKLOAD_CHECK // <-- Include this line is this feature is required + + +// For most single-load Mk2 systems, the power-diversion logic can operate in either of two modes: +// +// - NORMAL, where the triac switches rapidly on/off to maintain a constant energy level. +// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations +// of the local mains voltage. +// +// For this multi-load version, the same mechanism has been retained but the +// output mode is hard-coded as below: +enum outputModes outputMode = ANTI_FLICKER; + +// In this multi-load version, the external switch is re-used to determine the load priority +enum loadPriorityModes loadPriorityMode = LOAD_0_HAS_PRIORITY; + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +const byte physicalLoad_2_pin = 2; // <-- to control an additional load +const byte loadPrioritySelectorPin = 3; // <-- this is the "mode" port +const byte physicalLoad_0_pin = 4; // <-- this is the "trigger" port +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +const byte physicalLoad_3_pin = 10; // <-- to control an additional load +const byte physicalLoad_5_pin = 11; // <-- to control an additional load +const byte physicalLoad_1_pin = 12; // <-- to control an additional load +const byte physicalLoad_4_pin = 13; // <-- to control an additional load + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + + +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +long energyInBucket_long = 0; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long midPointOfEnergyBucket_long; // used for 'normal' and single-threshold 'AF' logic +int phaseCal_grid_int; // to avoid the need for floating-point maths +int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; + +long lowerThreshold_default; +long lowerEnergyThreshold; +long upperThreshold_default; +long upperEnergyThreshold; + +boolean recentTransition; +byte postTransitionCount; +#define POST_TRANSITION_MAX_COUNT 3 // <-- allows each transition to take effect +byte activeLoad = 0; + +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.4 + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +int sampleI_grid; +int sampleI_diverted; +int sampleV; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define POLARITY_CHECK_MAXCOUNT 2 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// With most hardware, the default values are likely to work fine without +// need for change. A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.044; // for CT2 + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When +// measuring a resistive load, the voltage and current waveforms should be perfectly +// aligned. In this situation, the Power Factor will be 1. +// +// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct +// value of phaseCal for any hardware configuration. An index of my various Mk2-related +// exhibits is available at http://openenergymonitor.org/emon/node/1757 +// +const float phaseCal_grid = 1.0; // default value +const float phaseCal_diverted = 1.0; // default value + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 10 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // Energy Diversion Detection (for the local load only) +long requiredExportPerMainsCycle_inIEU; +float IEUtoJoulesConversion_CT1; + +void setup() +{ + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for the local dump-load + pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_2_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_3_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_4_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_5_pin, OUTPUT); // driver pin for an additional load + + for(int i = 0; i< noOfDumploads; i++) + { + logicalLoadState[i] = LOAD_OFF; + physicalLoadState[i] = LOAD_OFF; + } + + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // the local load is active low. + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // additional loads are active high. + digitalWrite(physicalLoad_2_pin, !physicalLoadState[2]); // additional loads are active high + digitalWrite(physicalLoad_3_pin, !physicalLoadState[3]); // additional loads are active high + digitalWrite(physicalLoad_4_pin, !physicalLoadState[4]); // additional loads are active high + digitalWrite(physicalLoad_5_pin, !physicalLoadState[5]); // additional loads are active high + + pinMode(loadPrioritySelectorPin, INPUT); // this pin is tracked to the "mode" connector + digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and + loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority + + delay(5000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_multiLoad_wired_5a.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, calibration values that are supplied in floating point + // form need to be rescaled. + // + phaseCal_grid_int = phaseCal_grid * 256; // for integer maths + phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); +// energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up + midPointOfEnergyBucket_long = capacityOfEnergyBucket_long / 2; + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the correct scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples. The main processor and +// the ADC work autonomously, their operation being only linked via the dataReady flag. +// As soon as a new set of data is made available by the ADC, the main processor can +// start to work on it immediately. +// +void allGeneralProcessing() +{ + static boolean beyondStartUpPhase = false; // start-up delay, allows things to settle + static long sumP_grid; // for per-cycle summation of 'real power' + static long sumP_diverted; // for per-cycle summation of 'real power' + static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) + static long lastSampleVminusDC_long; // for the phaseCal algorithm + static byte perSecondCounter = 0; + + // remove DC offset from the raw voltage sample by subtracting the accurate value + // as determined by a LP filter. + long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long; + + // determine the polarity of the latest voltage sample + if(sampleVminusDC_long > 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + // cycleCount++; <- this mechanism is unsafe, it will eventually overflow + + // a simple routine for checking the continuity of the sampling scheme + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / sampleSetsDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- for non-standard use + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Applying max and min limits to bucket's level has been + // deferred until after energy-related decisions have been taken. + + if (EDD_isActive) // Energy Diversion Display + { + // When locally diverted energy is being monitored, the latest contribution + // needs to be added to an accumulator which operates with maximum precision. + // + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) { + realEnergy_diverted = 0; } // to avoid 'creep' + + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + perSecondCounter++; + if(perSecondCounter >= CYCLES_PER_SECOND) + { + perSecondCounter = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); // occurs every second +/* + Serial.print (energyInBucket_long * IEUtoJoulesConversion_CT1); + Serial.print (", "); + for (byte loadID = 0; loadID < noOfDumploads; loadID++) + { + Serial.print (!physicalLoadState[loadID]); // 1 = "on", 0 = "off" + Serial.print (" "); + } + Serial.println(); +*/ +// Serial.println(activeLoad); + } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + // check to see whether the trigger device can now be reliably armed + if(sampleSetsDuringThisMainsCycle == 3) // should always exceed 20V (the min for trigger) + { + /* Determining whether any of the loads need to be changed is is a 3-stage process: + * - change the LOGICAL load states as necessary to maintain the energy level + * - update the PHYSICAL load states according to the logical -> physical mapping + * - update the driver lines for each of the loads. + */ + + // Restrictions apply for the period immediately after a load has been switched. + // Here the recentTransition flag is checked and updated as necessary. + if (recentTransition) + { + postTransitionCount++; + if (postTransitionCount >= POST_TRANSITION_MAX_COUNT) + { + recentTransition = false; + } + } + + if (energyInBucket_long > midPointOfEnergyBucket_long) + { + // the energy state is in the upper half of the working range + lowerEnergyThreshold = lowerThreshold_default; // reset the "opposite" threshold + if (energyInBucket_long > upperEnergyThreshold) + { + // Because the energy level is high, some action may be required + boolean OK_toAddLoad = true; + byte tempLoad = nextLogicalLoadToBeAdded(); + if (tempLoad < noOfDumploads) + { + // a load which is now OFF has been identified for potentially being switched ON + if (recentTransition) + { + // During the post-transition period, any increase in the energy level is noted. + if (energyInBucket_long > upperEnergyThreshold) { + upperEnergyThreshold = energyInBucket_long; } + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toAddLoad = false; + } + } + + if (OK_toAddLoad) + { + logicalLoadState[tempLoad] = LOAD_ON; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + } + } + } + } + else + { // the energy state is in the lower half of the working range + upperEnergyThreshold = upperThreshold_default; // reset the "opposite" threshold + if (energyInBucket_long < lowerEnergyThreshold) + { + // Because the energy level is low, some action may be required + boolean OK_toRemoveLoad = true; + byte tempLoad = nextLogicalLoadToBeRemoved(); + if (tempLoad < noOfDumploads) + { + // a load which is now ON has been identified for potentially being switched OFF + if (recentTransition) + { + // During the post-transition period, any decrease in the energy level is noted. + if (energyInBucket_long < lowerEnergyThreshold) { + lowerEnergyThreshold = energyInBucket_long; } + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toRemoveLoad = false; + } + } + + if (OK_toRemoveLoad) + { + logicalLoadState[tempLoad] = LOAD_OFF; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + } + } + } + } + + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + // update each of the physical loads + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // active high for additional load + digitalWrite(physicalLoad_2_pin, !physicalLoadState[2]); // active high for additional load + digitalWrite(physicalLoad_3_pin, !physicalLoadState[3]); // active high for additional load + digitalWrite(physicalLoad_4_pin, !physicalLoadState[4]); // active high for additional load + digitalWrite(physicalLoad_5_pin, !physicalLoadState[5]); // active high for additional load + + // update the Energy Diversion Detector + if (physicalLoadState[0] == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + } + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > startUpPeriod * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringThisMainsCycle = 0; + sampleCount_forContinuityChecker = 0; + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + +// checkOutputModeSelection(); // updates outputMode if switch is changed + checkLoadPrioritySelection(); // updates load priorities if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is positive + + // processing for EVERY pair of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform + long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); +// long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform + long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); +// long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + sampleSetsDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + + refreshDisplay(); +} + + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count >= POLARITY_CHECK_MAXCOUNT) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + + +byte nextLogicalLoadToBeAdded() +{ + byte retVal = noOfDumploads; + boolean success = false; + for (byte index = 0; index < noOfDumploads && !success; index++) + { + if (logicalLoadState[index] == LOAD_OFF) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +byte nextLogicalLoadToBeRemoved() +{ + byte retVal = noOfDumploads; + boolean success = false; + for (byte index = (noOfDumploads -1); index >= 0 && !success; index--) + { + if (logicalLoadState[index] == LOAD_ON) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * The association between the physical and logical loads is 1:1. By default, numerical + * equivalence is maintained, so logical(N) maps to physical(N). If physical load 1 is set + * to have priority, rather than physical load 0, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * Any other mapping relaionships could be configured here. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } + + if (loadPriorityMode == LOAD_1_HAS_PRIORITY) + { + // swap physical loads 0 & 1 if remote load has priority + physicalLoadState[0] = logicalLoadState[1]; + physicalLoadState[1] = logicalLoadState[0]; + } +} + + +// this function changes the value of the load priorities if the state of the external switch is altered +void checkLoadPrioritySelection() +{ + static byte loadPrioritySwitchCount = 0; + int pinState = digitalRead(loadPrioritySelectorPin); + if (pinState != loadPriorityMode) + { + loadPrioritySwitchCount++; + } + if (loadPrioritySwitchCount >= 20) + { + loadPrioritySwitchCount = 0; + loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable + Serial.print ("loadPriority selection changed to "); + if (loadPriorityMode == LOAD_0_HAS_PRIORITY) { + Serial.println ( "load 0"); } + else { + Serial.println ( "load 1"); } + } +} + + +// Although this sketch always operates in ANTI_FLICKER mode, it was convenient +// to leave this mechanism in place. +// +void configureParamsForSelectedOutputMode() +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerThreshold_default = + capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperThreshold_default = + capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerThreshold_default = capacityOfEnergyBucket_long * 0.5; + upperThreshold_default = capacityOfEnergyBucket_long * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" lowerEnergyThreshold = "); + Serial.println(lowerThreshold_default); + Serial.print(" upperEnergyThreshold = "); + Serial.println(upperThreshold_default); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +// valueToBeDisplayed++; +} + + + + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_wired_6.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_wired_6.ino new file mode 100644 index 0000000..e8d18a4 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_wired_6.ino @@ -0,0 +1,1270 @@ +/* Mk2_multiLoad_wired_6.ino <--- suitable for use (with fast switching of loads) + * is based on Mk2_multiLoad_wired_5a.ino <--- not to be used + * which is based on Mk2_multiLoad_wired_5.ino <--- not to be used + * which is based on Mk2_multiLoad_CAT5_4.ino <--- suitable for use (with slower switching of loads) + * + * This sketch is for diverting surplus PV power using multiple hard-wired loads. + * An external switch allows either load 0 or Load 1 to have the highest + * priority. Any number of loads can be supported by the logic, a dedicated + * IO pin being required for each one. + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The selector switch, as mentioned above, connects to the "mode" port which has been + * re-assigned for priority selection. "Normal" mode can be achieved by setting the + * anti-flicker offset prameter to zero at compile-time. + * + * The integral voltage sensor is fed from one of the secondary coils of the transformer. + * Current is measured via Current Transformers at the CT1 and CT2 ports. + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the 'diverted' current, so that energy which is diverted via the primary + * dump-load can be recorded and displayed locally. + * + * A persistence-based 4-digit display is supported. To free up the necessary IO pins + * for driving multiple loads, the pin-saving hardware needs to be in place. + * These extra logic chips (ICs 3 and 4) reduce the number of IO pins + * that are needed to drive the display. The freed-up pins are available at the + * J1-5 connector. The uppermost position has been assigned to drive Load 1, + * the next one down is for Load 2, and the lowest one is for Load 5. The control signal + * for Load 0 is available at the "trigger" connector. + * + * With the green (rev 2.1) version of my PCB, each of the additional outputs has an + * associated ground pin. It is therefore more sensible for those outputs to be active-high + * rather than active-low. With a 5V regulator rather than the normal 3.3V one, these + * outputs are able to drive an SSR directly. energymonitor.org/emon/node/1757 + * + * September 2014: renamed as Mk2_multiLoad_CAT5_3, with these changes: + * - reimplementation of cycleCount, as it could have overflowed with unpredictable results; + * - the functions increaseLoadIfPossible() and decreaseLoadIfPossible() have been tidied; + * - energyThreshold_long has been renamed as midPointOfEnergyBucket_long. + * + * December 2014: renamed as Mk2_multiLoad_CAT5_4, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed); + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances; + * - the logic for each of the 5 additional loads has been inverted by use of the '!' character. + * these outputs are now active-high rather than active-low + * + * November 2015: renamed as Mk2_multiLoad_wired_5, with these changes: + * - the original twin-threshold algorithm for energy state management has been reinstated; + * - improved mechanism for controlling multiple loads (faster and more accurate); + * - the phaseCal mechanism has been reinstated; + * - SWEETZONE_IN_JOULES has been replaced by WORKING_RANGE_IN_JOULES. + * + * January 2015: renamed as Mk2_multiLoad_wired_5a, with this change: + * - minor bug-fix in allGeneralProcessing() which affects how the energy thresholds are adjusted immediately + * after a change of load-state has tqaken place. + * + * January 2015: renamed as Mk2_multiLoad_wired_6, with this change: + * - reinstatement of min & max limits for the energy bucket's level. This section was lost during the + * conversion from version 4 to version 5. The absence of this section prevents diversion from starting + * in the correct manner. Versions 5 and 5a should therefore not be used. Version 6 is believed to be + * a correct implementation of the improved mechanism for controlling multiple loads. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define WORKING_RANGE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +const byte noOfDumploads = 6; // The logic expects a minimum of 2 dumploads, + // for local & remote loads, but neither has to + // be physically present. + +// definitions of enumerated types and instances of same +enum polarities {NEGATIVE, POSITIVE}; +enum outputModes {ANTI_FLICKER, NORMAL}; +enum loadPriorityModes {LOAD_1_HAS_PRIORITY, LOAD_0_HAS_PRIORITY}; + +enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are active low +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +// ---------------- Extra Features selection ---------------------- +// +// - WORKLOAD_CHECK, for determining how much spare processing time there is. +// +// #define WORKLOAD_CHECK // <-- Include this line is this feature is required + + +// For most single-load Mk2 systems, the power-diversion logic can operate in either of two modes: +// +// - NORMAL, where the triac switches rapidly on/off to maintain a constant energy level. +// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations +// of the local mains voltage. +// +// For this multi-load version, the same mechanism has been retained but the +// output mode is hard-coded as below: +enum outputModes outputMode = ANTI_FLICKER; + +// In this multi-load version, the external switch is re-used to determine the load priority +enum loadPriorityModes loadPriorityMode = LOAD_0_HAS_PRIORITY; + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +const byte physicalLoad_2_pin = 2; // <-- to control an additional load +const byte loadPrioritySelectorPin = 3; // <-- this is the "mode" port +const byte physicalLoad_0_pin = 4; // <-- this is the "trigger" port +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +const byte physicalLoad_3_pin = 10; // <-- to control an additional load +const byte physicalLoad_5_pin = 11; // <-- to control an additional load +const byte physicalLoad_1_pin = 12; // <-- to control an additional load +const byte physicalLoad_4_pin = 13; // <-- to control an additional load + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + + +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +long energyInBucket_long = 0; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long midPointOfEnergyBucket_long; // used for 'normal' and single-threshold 'AF' logic +int phaseCal_grid_int; // to avoid the need for floating-point maths +int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; + +long lowerThreshold_default; +long lowerEnergyThreshold; +long upperThreshold_default; +long upperEnergyThreshold; + +boolean recentTransition; +byte postTransitionCount; +#define POST_TRANSITION_MAX_COUNT 3 // <-- allows each transition to take effect +byte activeLoad = 0; + +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.4 + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +int sampleI_grid; +int sampleI_diverted; +int sampleV; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define POLARITY_CHECK_MAXCOUNT 2 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// With most hardware, the default values are likely to work fine without +// need for change. A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.044; // for CT2 + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When +// measuring a resistive load, the voltage and current waveforms should be perfectly +// aligned. In this situation, the Power Factor will be 1. +// +// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct +// value of phaseCal for any hardware configuration. An index of my various Mk2-related +// exhibits is available at http://openenergymonitor.org/emon/node/1757 +// +const float phaseCal_grid = 1.0; // default value +const float phaseCal_diverted = 1.0; // default value + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 10 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // Energy Diversion Detection (for the local load only) +long requiredExportPerMainsCycle_inIEU; +float IEUtoJoulesConversion_CT1; + +void setup() +{ + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for the local dump-load + pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_2_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_3_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_4_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_5_pin, OUTPUT); // driver pin for an additional load + + for(int i = 0; i< noOfDumploads; i++) + { + logicalLoadState[i] = LOAD_OFF; + physicalLoadState[i] = LOAD_OFF; + } + + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // the local load is active low. + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // additional loads are active high. + digitalWrite(physicalLoad_2_pin, !physicalLoadState[2]); // additional loads are active high + digitalWrite(physicalLoad_3_pin, !physicalLoadState[3]); // additional loads are active high + digitalWrite(physicalLoad_4_pin, !physicalLoadState[4]); // additional loads are active high + digitalWrite(physicalLoad_5_pin, !physicalLoadState[5]); // additional loads are active high + + pinMode(loadPrioritySelectorPin, INPUT); // this pin is tracked to the "mode" connector + digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and + loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority + + delay(5000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_multiLoad_wired_6.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, calibration values that are supplied in floating point + // form need to be rescaled. + // + phaseCal_grid_int = phaseCal_grid * 256; // for integer maths + phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); +// energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up + midPointOfEnergyBucket_long = capacityOfEnergyBucket_long / 2; + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the correct scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples. The main processor and +// the ADC work autonomously, their operation being only linked via the dataReady flag. +// As soon as a new set of data is made available by the ADC, the main processor can +// start to work on it immediately. +// +void allGeneralProcessing() +{ + static boolean beyondStartUpPhase = false; // start-up delay, allows things to settle + static long sumP_grid; // for per-cycle summation of 'real power' + static long sumP_diverted; // for per-cycle summation of 'real power' + static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) + static long lastSampleVminusDC_long; // for the phaseCal algorithm + static byte perSecondCounter = 0; + + // remove DC offset from the raw voltage sample by subtracting the accurate value + // as determined by a LP filter. + long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long; + + // determine the polarity of the latest voltage sample + if(sampleVminusDC_long > 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + // cycleCount++; <- this mechanism is unsafe, it will eventually overflow + + // a simple routine for checking the continuity of the sampling scheme + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / sampleSetsDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- for non-standard use + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Applying max and min limits to bucket's level has been + // deferred until after energy-related decisions have been taken. + + if (EDD_isActive) // Energy Diversion Display + { + // When locally diverted energy is being monitored, the latest contribution + // needs to be added to an accumulator which operates with maximum precision. + // + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) { + realEnergy_diverted = 0; } // to avoid 'creep' + + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + perSecondCounter++; + if(perSecondCounter >= CYCLES_PER_SECOND) + { + perSecondCounter = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); // occurs every second +/* + Serial.print (energyInBucket_long * IEUtoJoulesConversion_CT1); + Serial.print (", "); + for (byte loadID = 0; loadID < noOfDumploads; loadID++) + { + Serial.print (!physicalLoadState[loadID]); // 1 = "on", 0 = "off" + Serial.print (" "); + } + Serial.println(); +*/ +// Serial.println(activeLoad); + } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + // check to see whether the trigger device can now be reliably armed + if(sampleSetsDuringThisMainsCycle == 3) // should always exceed 20V (the min for trigger) + { + /* Determining whether any of the loads need to be changed is is a 3-stage process: + * - change the LOGICAL load states as necessary to maintain the energy level + * - update the PHYSICAL load states according to the logical -> physical mapping + * - update the driver lines for each of the loads. + */ + + // Restrictions apply for the period immediately after a load has been switched. + // Here the recentTransition flag is checked and updated as necessary. + if (recentTransition) + { + postTransitionCount++; + if (postTransitionCount >= POST_TRANSITION_MAX_COUNT) + { + recentTransition = false; + } + } + + if (energyInBucket_long > midPointOfEnergyBucket_long) + { + // the energy state is in the upper half of the working range + lowerEnergyThreshold = lowerThreshold_default; // reset the "opposite" threshold + if (energyInBucket_long > upperEnergyThreshold) + { + // Because the energy level is high, some action may be required + boolean OK_toAddLoad = true; + byte tempLoad = nextLogicalLoadToBeAdded(); + if (tempLoad < noOfDumploads) + { + // a load which is now OFF has been identified for potentially being switched ON + if (recentTransition) + { + // During the post-transition period, any increase in the energy level is noted. + if (energyInBucket_long > upperEnergyThreshold) + { + upperEnergyThreshold = energyInBucket_long; + + // the energy thresholds must remain within range + if (upperEnergyThreshold > capacityOfEnergyBucket_long) + { + upperEnergyThreshold = capacityOfEnergyBucket_long; + } + } + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toAddLoad = false; + } + } + + if (OK_toAddLoad) + { + logicalLoadState[tempLoad] = LOAD_ON; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + } + } + } + } + else + { // the energy state is in the lower half of the working range + upperEnergyThreshold = upperThreshold_default; // reset the "opposite" threshold + if (energyInBucket_long < lowerEnergyThreshold) + { + // Because the energy level is low, some action may be required + boolean OK_toRemoveLoad = true; + byte tempLoad = nextLogicalLoadToBeRemoved(); + if (tempLoad < noOfDumploads) + { + // a load which is now ON has been identified for potentially being switched OFF + if (recentTransition) + { + // During the post-transition period, any decrease in the energy level is noted. + if (energyInBucket_long < lowerEnergyThreshold) + { + lowerEnergyThreshold = energyInBucket_long; + + // the energy thresholds must remain within range + if (lowerEnergyThreshold < 0) + { + lowerEnergyThreshold = 0; + } + } + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toRemoveLoad = false; + } + } + + if (OK_toRemoveLoad) + { + logicalLoadState[tempLoad] = LOAD_OFF; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + } + } + } + } + + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + // update each of the physical loads + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // active high for additional load + digitalWrite(physicalLoad_2_pin, !physicalLoadState[2]); // active high for additional load + digitalWrite(physicalLoad_3_pin, !physicalLoadState[3]); // active high for additional load + digitalWrite(physicalLoad_4_pin, !physicalLoadState[4]); // active high for additional load + digitalWrite(physicalLoad_5_pin, !physicalLoadState[5]); // active high for additional load + + // update the Energy Diversion Detector + if (physicalLoadState[0] == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + + // Now that the energy-related decisions have been taken, min and max limits can now + // be applied to the level of the energy bucket. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + } + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > startUpPeriod * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringThisMainsCycle = 0; + sampleCount_forContinuityChecker = 0; + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + +// checkOutputModeSelection(); // updates outputMode if switch is changed + checkLoadPrioritySelection(); // updates load priorities if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is positive + + // processing for EVERY pair of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform + long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); +// long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform + long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); +// long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + sampleSetsDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + + refreshDisplay(); +} + + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count >= POLARITY_CHECK_MAXCOUNT) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + + +byte nextLogicalLoadToBeAdded() +{ + byte retVal = noOfDumploads; + boolean success = false; + for (byte index = 0; index < noOfDumploads && !success; index++) + { + if (logicalLoadState[index] == LOAD_OFF) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +byte nextLogicalLoadToBeRemoved() +{ + byte retVal = noOfDumploads; + boolean success = false; + + // this index counter can't be a 'byte' because the loop would run forever! + // + for (int index = (noOfDumploads -1); index >= 0 && !success; index--) + { + if (logicalLoadState[index] == LOAD_ON) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * The association between the physical and logical loads is 1:1. By default, numerical + * equivalence is maintained, so logical(N) maps to physical(N). If physical load 1 is set + * to have priority, rather than physical load 0, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * Any other mapping relaionships could be configured here. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } + + if (loadPriorityMode == LOAD_1_HAS_PRIORITY) + { + // swap physical loads 0 & 1 if remote load has priority + physicalLoadState[0] = logicalLoadState[1]; + physicalLoadState[1] = logicalLoadState[0]; + } +} + + +// this function changes the value of the load priorities if the state of the external switch is altered +void checkLoadPrioritySelection() +{ + static byte loadPrioritySwitchCount = 0; + int pinState = digitalRead(loadPrioritySelectorPin); + if (pinState != loadPriorityMode) + { + loadPrioritySwitchCount++; + } + if (loadPrioritySwitchCount >= 20) + { + loadPrioritySwitchCount = 0; + loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable + Serial.print ("loadPriority selection changed to "); + if (loadPriorityMode == LOAD_0_HAS_PRIORITY) { + Serial.println ( "load 0"); } + else { + Serial.println ( "load 1"); } + } +} + + +// Although this sketch always operates in ANTI_FLICKER mode, it was convenient +// to leave this mechanism in place. +// +void configureParamsForSelectedOutputMode() +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerThreshold_default = + capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperThreshold_default = + capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerThreshold_default = capacityOfEnergyBucket_long * 0.5; + upperThreshold_default = capacityOfEnergyBucket_long * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" lowerEnergyThreshold = "); + Serial.println(lowerThreshold_default); + Serial.print(" upperEnergyThreshold = "); + Serial.println(upperThreshold_default); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +// valueToBeDisplayed++; +} + + + + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_wired_6a.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_wired_6a.ino new file mode 100644 index 0000000..fc6622f --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_wired_6a.ino @@ -0,0 +1,1278 @@ +/* Mk2_multiLoad_wired_6a.ino <--- suitable for use (with fast switching of loads) + * is based on Mk2_multiLoad_wired_6.ino <--- suitable for use (with fast switching of loads) + * is based on Mk2_multiLoad_wired_5a.ino <--- not to be used + * which is based on Mk2_multiLoad_wired_5.ino <--- not to be used + * which is based on Mk2_multiLoad_CAT5_4.ino <--- suitable for use (with slower switching of loads) + * + * This sketch is for diverting surplus PV power using multiple hard-wired loads. + * An external switch allows either load 0 or Load 1 to have the highest + * priority. Any number of loads can be supported by the logic, a dedicated + * IO pin being required for each one. + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The selector switch, as mentioned above, connects to the "mode" port which has been + * re-assigned for priority selection. "Normal" mode can be achieved by setting the + * anti-flicker offset prameter to zero at compile-time. + * + * The integral voltage sensor is fed from one of the secondary coils of the transformer. + * Current is measured via Current Transformers at the CT1 and CT2 ports. + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the 'diverted' current, so that energy which is diverted via the primary + * dump-load can be recorded and displayed locally. + * + * A persistence-based 4-digit display is supported. To free up the necessary IO pins + * for driving multiple loads, the pin-saving hardware needs to be in place. + * These extra logic chips (ICs 3 and 4) reduce the number of IO pins + * that are needed to drive the display. The freed-up pins are available at the + * J1-5 connector. The uppermost position has been assigned to drive Load 1, + * the next one down is for Load 2, and the lowest one is for Load 5. The control signal + * for Load 0 is available at the "trigger" connector. + * + * With the green (rev 2.1) version of my PCB, each of the additional outputs has an + * associated ground pin. It is therefore more sensible for those outputs to be active-high + * rather than active-low. With a 5V regulator rather than the normal 3.3V one, these + * outputs are able to drive an SSR directly. energymonitor.org/emon/node/1757 + * + * September 2014: renamed as Mk2_multiLoad_CAT5_3, with these changes: + * - reimplementation of cycleCount, as it could have overflowed with unpredictable results; + * - the functions increaseLoadIfPossible() and decreaseLoadIfPossible() have been tidied; + * - energyThreshold_long has been renamed as midPointOfEnergyBucket_long. + * + * December 2014: renamed as Mk2_multiLoad_CAT5_4, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed); + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances; + * - the logic for each of the 5 additional loads has been inverted by use of the '!' character. + * these outputs are now active-high rather than active-low + * + * November 2015: renamed as Mk2_multiLoad_wired_5, with these changes: + * - the original twin-threshold algorithm for energy state management has been reinstated; + * - improved mechanism for controlling multiple loads (faster and more accurate); + * - the phaseCal mechanism has been reinstated; + * - SWEETZONE_IN_JOULES has been replaced by WORKING_RANGE_IN_JOULES. + * + * January 2015: renamed as Mk2_multiLoad_wired_5a, with this change: + * - minor bug-fix in allGeneralProcessing() which affects how the energy thresholds are adjusted immediately + * after a change of load-state has tqaken place. + * + * January 2015: renamed as Mk2_multiLoad_wired_6, with this change: + * - reinstatement of min & max limits for the energy bucket's level. This section was lost during the + * conversion from version 4 to version 5. The absence of this section prevents diversion from starting + * in the correct manner. Versions 5 and 5a should therefore not be used. Version 6 is believed to be + * a correct implementation of the improved mechanism for controlling multiple loads. + * + * January 2016: renamed as Mk2_multiLoad_wired_6a, with a minor change in the ISR to + * remove a timing uncertainty. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define WORKING_RANGE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +const byte noOfDumploads = 6; // The logic expects a minimum of 2 dumploads, + // for local & remote loads, but neither has to + // be physically present. + +// definitions of enumerated types and instances of same +enum polarities {NEGATIVE, POSITIVE}; +enum outputModes {ANTI_FLICKER, NORMAL}; +enum loadPriorityModes {LOAD_1_HAS_PRIORITY, LOAD_0_HAS_PRIORITY}; + +enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are active low +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +// ---------------- Extra Features selection ---------------------- +// +// - WORKLOAD_CHECK, for determining how much spare processing time there is. +// +// #define WORKLOAD_CHECK // <-- Include this line is this feature is required + + +// For most single-load Mk2 systems, the power-diversion logic can operate in either of two modes: +// +// - NORMAL, where the triac switches rapidly on/off to maintain a constant energy level. +// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations +// of the local mains voltage. +// +// For this multi-load version, the same mechanism has been retained but the +// output mode is hard-coded as below: +enum outputModes outputMode = ANTI_FLICKER; + +// In this multi-load version, the external switch is re-used to determine the load priority +enum loadPriorityModes loadPriorityMode = LOAD_0_HAS_PRIORITY; + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +const byte physicalLoad_2_pin = 2; // <-- to control an additional load +const byte loadPrioritySelectorPin = 3; // <-- this is the "mode" port +const byte physicalLoad_0_pin = 4; // <-- this is the "trigger" port +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +const byte physicalLoad_3_pin = 10; // <-- to control an additional load +const byte physicalLoad_5_pin = 11; // <-- to control an additional load +const byte physicalLoad_1_pin = 12; // <-- to control an additional load +const byte physicalLoad_4_pin = 13; // <-- to control an additional load + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + + +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +long energyInBucket_long = 0; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long midPointOfEnergyBucket_long; // used for 'normal' and single-threshold 'AF' logic +int phaseCal_grid_int; // to avoid the need for floating-point maths +int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; + +long lowerThreshold_default; +long lowerEnergyThreshold; +long upperThreshold_default; +long upperEnergyThreshold; + +boolean recentTransition; +byte postTransitionCount; +#define POST_TRANSITION_MAX_COUNT 3 // <-- allows each transition to take effect +byte activeLoad = 0; + +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.4 + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +int sampleI_grid; +int sampleI_diverted; +int sampleV; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define POLARITY_CHECK_MAXCOUNT 2 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// With most hardware, the default values are likely to work fine without +// need for change. A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.044; // for CT2 + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When +// measuring a resistive load, the voltage and current waveforms should be perfectly +// aligned. In this situation, the Power Factor will be 1. +// +// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct +// value of phaseCal for any hardware configuration. An index of my various Mk2-related +// exhibits is available at http://openenergymonitor.org/emon/node/1757 +// +const float phaseCal_grid = 1.0; // default value +const float phaseCal_diverted = 1.0; // default value + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // Energy Diversion Detection (for the local load only) +long requiredExportPerMainsCycle_inIEU; +float IEUtoJoulesConversion_CT1; + +void setup() +{ + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for the local dump-load + pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_2_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_3_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_4_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_5_pin, OUTPUT); // driver pin for an additional load + + for(int i = 0; i< noOfDumploads; i++) + { + logicalLoadState[i] = LOAD_OFF; + physicalLoadState[i] = LOAD_OFF; + } + + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // the local load is active low. + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // additional loads are active high. + digitalWrite(physicalLoad_2_pin, !physicalLoadState[2]); // additional loads are active high + digitalWrite(physicalLoad_3_pin, !physicalLoadState[3]); // additional loads are active high + digitalWrite(physicalLoad_4_pin, !physicalLoadState[4]); // additional loads are active high + digitalWrite(physicalLoad_5_pin, !physicalLoadState[5]); // additional loads are active high + + pinMode(loadPrioritySelectorPin, INPUT); // this pin is tracked to the "mode" connector + digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and + loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority + + delay(5000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_multiLoad_wired_6a.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, calibration values that are supplied in floating point + // form need to be rescaled. + // + phaseCal_grid_int = phaseCal_grid * 256; // for integer maths + phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); +// energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up + midPointOfEnergyBucket_long = capacityOfEnergyBucket_long / 2; + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the correct scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples. The main processor and +// the ADC work autonomously, their operation being only linked via the dataReady flag. +// As soon as a new set of data is made available by the ADC, the main processor can +// start to work on it immediately. +// +void allGeneralProcessing() +{ + static boolean beyondStartUpPhase = false; // start-up delay, allows things to settle + static long sumP_grid; // for per-cycle summation of 'real power' + static long sumP_diverted; // for per-cycle summation of 'real power' + static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) + static long lastSampleVminusDC_long; // for the phaseCal algorithm + static byte perSecondCounter = 0; + + // remove DC offset from the raw voltage sample by subtracting the accurate value + // as determined by a LP filter. + long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long; + + // determine the polarity of the latest voltage sample + if(sampleVminusDC_long > 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + // cycleCount++; <- this mechanism is unsafe, it will eventually overflow + + // a simple routine for checking the continuity of the sampling scheme + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / sampleSetsDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- for non-standard use + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Applying max and min limits to bucket's level has been + // deferred until after energy-related decisions have been taken. + + if (EDD_isActive) // Energy Diversion Display + { + // When locally diverted energy is being monitored, the latest contribution + // needs to be added to an accumulator which operates with maximum precision. + // + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) { + realEnergy_diverted = 0; } // to avoid 'creep' + + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + perSecondCounter++; + if(perSecondCounter >= CYCLES_PER_SECOND) + { + perSecondCounter = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); // occurs every second +/* + Serial.print (energyInBucket_long * IEUtoJoulesConversion_CT1); + Serial.print (", "); + for (byte loadID = 0; loadID < noOfDumploads; loadID++) + { + Serial.print (!physicalLoadState[loadID]); // 1 = "on", 0 = "off" + Serial.print (" "); + } + Serial.println(); +*/ +// Serial.println(activeLoad); + } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + // check to see whether the trigger device can now be reliably armed + if(sampleSetsDuringThisMainsCycle == 3) // should always exceed 20V (the min for trigger) + { + /* Determining whether any of the loads need to be changed is is a 3-stage process: + * - change the LOGICAL load states as necessary to maintain the energy level + * - update the PHYSICAL load states according to the logical -> physical mapping + * - update the driver lines for each of the loads. + */ + + // Restrictions apply for the period immediately after a load has been switched. + // Here the recentTransition flag is checked and updated as necessary. + if (recentTransition) + { + postTransitionCount++; + if (postTransitionCount >= POST_TRANSITION_MAX_COUNT) + { + recentTransition = false; + } + } + + if (energyInBucket_long > midPointOfEnergyBucket_long) + { + // the energy state is in the upper half of the working range + lowerEnergyThreshold = lowerThreshold_default; // reset the "opposite" threshold + if (energyInBucket_long > upperEnergyThreshold) + { + // Because the energy level is high, some action may be required + boolean OK_toAddLoad = true; + byte tempLoad = nextLogicalLoadToBeAdded(); + if (tempLoad < noOfDumploads) + { + // a load which is now OFF has been identified for potentially being switched ON + if (recentTransition) + { + // During the post-transition period, any increase in the energy level is noted. + if (energyInBucket_long > upperEnergyThreshold) + { + upperEnergyThreshold = energyInBucket_long; + + // the energy thresholds must remain within range + if (upperEnergyThreshold > capacityOfEnergyBucket_long) + { + upperEnergyThreshold = capacityOfEnergyBucket_long; + } + } + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toAddLoad = false; + } + } + + if (OK_toAddLoad) + { + logicalLoadState[tempLoad] = LOAD_ON; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + } + } + } + } + else + { // the energy state is in the lower half of the working range + upperEnergyThreshold = upperThreshold_default; // reset the "opposite" threshold + if (energyInBucket_long < lowerEnergyThreshold) + { + // Because the energy level is low, some action may be required + boolean OK_toRemoveLoad = true; + byte tempLoad = nextLogicalLoadToBeRemoved(); + if (tempLoad < noOfDumploads) + { + // a load which is now ON has been identified for potentially being switched OFF + if (recentTransition) + { + // During the post-transition period, any decrease in the energy level is noted. + if (energyInBucket_long < lowerEnergyThreshold) + { + lowerEnergyThreshold = energyInBucket_long; + + // the energy thresholds must remain within range + if (lowerEnergyThreshold < 0) + { + lowerEnergyThreshold = 0; + } + } + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toRemoveLoad = false; + } + } + + if (OK_toRemoveLoad) + { + logicalLoadState[tempLoad] = LOAD_OFF; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + } + } + } + } + + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + // update each of the physical loads + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // active high for additional load + digitalWrite(physicalLoad_2_pin, !physicalLoadState[2]); // active high for additional load + digitalWrite(physicalLoad_3_pin, !physicalLoadState[3]); // active high for additional load + digitalWrite(physicalLoad_4_pin, !physicalLoadState[4]); // active high for additional load + digitalWrite(physicalLoad_5_pin, !physicalLoadState[5]); // active high for additional load + + // update the Energy Diversion Detector + if (physicalLoadState[0] == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + + // Now that the energy-related decisions have been taken, min and max limits can now + // be applied to the level of the energy bucket. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + } + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > startUpPeriod * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringThisMainsCycle = 0; + sampleCount_forContinuityChecker = 0; + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + +// checkOutputModeSelection(); // updates outputMode if switch is changed + checkLoadPrioritySelection(); // updates load priorities if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is positive + + // processing for EVERY pair of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform + long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); +// long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform + long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); +// long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + sampleSetsDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + + refreshDisplay(); +} + + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count >= POLARITY_CHECK_MAXCOUNT) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + + +byte nextLogicalLoadToBeAdded() +{ + byte retVal = noOfDumploads; + boolean success = false; + for (byte index = 0; index < noOfDumploads && !success; index++) + { + if (logicalLoadState[index] == LOAD_OFF) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +byte nextLogicalLoadToBeRemoved() +{ + byte retVal = noOfDumploads; + boolean success = false; + + // this index counter can't be a 'byte' because the loop would run forever! + // + for (int index = (noOfDumploads -1); index >= 0 && !success; index--) + { + if (logicalLoadState[index] == LOAD_ON) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * The association between the physical and logical loads is 1:1. By default, numerical + * equivalence is maintained, so logical(N) maps to physical(N). If physical load 1 is set + * to have priority, rather than physical load 0, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * Any other mapping relaionships could be configured here. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } + + if (loadPriorityMode == LOAD_1_HAS_PRIORITY) + { + // swap physical loads 0 & 1 if remote load has priority + physicalLoadState[0] = logicalLoadState[1]; + physicalLoadState[1] = logicalLoadState[0]; + } +} + + +// this function changes the value of the load priorities if the state of the external switch is altered +void checkLoadPrioritySelection() +{ + static byte loadPrioritySwitchCount = 0; + int pinState = digitalRead(loadPrioritySelectorPin); + if (pinState != loadPriorityMode) + { + loadPrioritySwitchCount++; + } + if (loadPrioritySwitchCount >= 20) + { + loadPrioritySwitchCount = 0; + loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable + Serial.print ("loadPriority selection changed to "); + if (loadPriorityMode == LOAD_0_HAS_PRIORITY) { + Serial.println ( "load 0"); } + else { + Serial.println ( "load 1"); } + } +} + + +// Although this sketch always operates in ANTI_FLICKER mode, it was convenient +// to leave this mechanism in place. +// +void configureParamsForSelectedOutputMode() +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerThreshold_default = + capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperThreshold_default = + capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerThreshold_default = capacityOfEnergyBucket_long * 0.5; + upperThreshold_default = capacityOfEnergyBucket_long * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" lowerEnergyThreshold = "); + Serial.println(lowerThreshold_default); + Serial.print(" upperEnergyThreshold = "); + Serial.println(upperThreshold_default); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +// valueToBeDisplayed++; +} + + + + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_wired_6b.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_wired_6b.ino new file mode 100644 index 0000000..a1c5ac5 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_wired_6b.ino @@ -0,0 +1,1278 @@ +/* Mk2_multiLoad_wired_6b.ino + * + * This sketch is for diverting surplus PV power using multiple hard-wired loads. + * An external switch allows either load 0 or Load 1 to have the highest + * priority. Any number of loads can be supported by the logic, a dedicated + * IO pin being required for each one. + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The selector switch, as mentioned above, connects to the "mode" port which has been + * re-assigned for priority selection. "Normal" mode can be achieved by setting the + * anti-flicker offset prameter to zero at compile-time. + * + * The integral voltage sensor is fed from one of the secondary coils of the transformer. + * Current is measured via Current Transformers at the CT1 and CT2 ports. + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the 'diverted' current, so that energy which is diverted via the primary + * dump-load can be recorded and displayed locally. + * + * A persistence-based 4-digit display is supported. To free up the necessary IO pins + * for driving multiple loads, the pin-saving hardware needs to be in place. + * These extra logic chips (ICs 3 and 4) reduce the number of IO pins + * that are needed to drive the display. The freed-up pins are available at the + * J1-5 connector. The uppermost position has been assigned to drive Load 1, + * the next one down is for Load 2, and the lowest one is for Load 5. The control signal + * for Load 0 is available at the "trigger" connector. + * + * With the green (rev 2.1) version of my PCB, each of the additional outputs has an + * associated ground pin. It is therefore more sensible for those outputs to be active-high + * rather than active-low. With a 5V regulator rather than the normal 3.3V one, these + * outputs are able to drive an SSR directly. energymonitor.org/emon/node/1757 + * + * September 2014: renamed as Mk2_multiLoad_CAT5_3, with these changes: + * - reimplementation of cycleCount, as it could have overflowed with unpredictable results; + * - the functions increaseLoadIfPossible() and decreaseLoadIfPossible() have been tidied; + * - energyThreshold_long has been renamed as midPointOfEnergyBucket_long. + * + * December 2014: renamed as Mk2_multiLoad_CAT5_4, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed); + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances; + * - the logic for each of the 5 additional loads has been inverted by use of the '!' character. + * these outputs are now active-high rather than active-low + * + * November 2015: renamed as Mk2_multiLoad_wired_5, with these changes: + * - the original twin-threshold algorithm for energy state management has been reinstated; + * - improved mechanism for controlling multiple loads (faster and more accurate); + * - the phaseCal mechanism has been reinstated; + * - SWEETZONE_IN_JOULES has been replaced by WORKING_RANGE_IN_JOULES. + * + * January 2015: renamed as Mk2_multiLoad_wired_5a, with this change: + * - minor bug-fix in allGeneralProcessing() which affects how the energy thresholds are adjusted immediately + * after a change of load-state has tqaken place. + * + * January 2015: renamed as Mk2_multiLoad_wired_6, with this change: + * - reinstatement of min & max limits for the energy bucket's level. This section was lost during the + * conversion from version 4 to version 5. The absence of this section prevents diversion from starting + * in the correct manner. Versions 5 and 5a should therefore not be used. Version 6 is believed to be + * a correct implementation of the improved mechanism for controlling multiple loads. + * + * January 2016: renamed as Mk2_multiLoad_wired_6a, with a minor change in the ISR to + * remove a timing uncertainty. + * + * January 2016: updated to Mk2_multiLoad_wired_6b: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define WORKING_RANGE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +const byte noOfDumploads = 6; // The logic expects a minimum of 2 dumploads, + // for local & remote loads, but neither has to + // be physically present. + +// definitions of enumerated types and instances of same +enum polarities {NEGATIVE, POSITIVE}; +enum outputModes {ANTI_FLICKER, NORMAL}; +enum loadPriorityModes {LOAD_1_HAS_PRIORITY, LOAD_0_HAS_PRIORITY}; + +enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are active low +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +// ---------------- Extra Features selection ---------------------- +// +// - WORKLOAD_CHECK, for determining how much spare processing time there is. +// +// #define WORKLOAD_CHECK // <-- Include this line is this feature is required + + +// For most single-load Mk2 systems, the power-diversion logic can operate in either of two modes: +// +// - NORMAL, where the triac switches rapidly on/off to maintain a constant energy level. +// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations +// of the local mains voltage. +// +// For this multi-load version, the same mechanism has been retained but the +// output mode is hard-coded as below: +enum outputModes outputMode = ANTI_FLICKER; + +// In this multi-load version, the external switch is re-used to determine the load priority +enum loadPriorityModes loadPriorityMode = LOAD_0_HAS_PRIORITY; + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +const byte physicalLoad_2_pin = 2; // <-- to control an additional load +const byte loadPrioritySelectorPin = 3; // <-- this is the "mode" port +const byte physicalLoad_0_pin = 4; // <-- this is the "trigger" port +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +const byte physicalLoad_3_pin = 10; // <-- to control an additional load +const byte physicalLoad_5_pin = 11; // <-- to control an additional load +const byte physicalLoad_1_pin = 12; // <-- to control an additional load +const byte physicalLoad_4_pin = 13; // <-- to control an additional load + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + + +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +long energyInBucket_long = 0; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long midPointOfEnergyBucket_long; // used for 'normal' and single-threshold 'AF' logic +int phaseCal_grid_int; // to avoid the need for floating-point maths +int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; + +long lowerThreshold_default; +long lowerEnergyThreshold; +long upperThreshold_default; +long upperEnergyThreshold; + +boolean recentTransition; +byte postTransitionCount; +#define POST_TRANSITION_MAX_COUNT 3 // <-- allows each transition to take effect +byte activeLoad = 0; + +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.4 + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +volatile int sampleI_grid; +volatile int sampleI_diverted; +volatile int sampleV; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define POLARITY_CHECK_MAXCOUNT 2 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// With most hardware, the default values are likely to work fine without +// need for change. A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.044; // for CT2 + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When +// measuring a resistive load, the voltage and current waveforms should be perfectly +// aligned. In this situation, the Power Factor will be 1. +// +// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct +// value of phaseCal for any hardware configuration. An index of my various Mk2-related +// exhibits is available at http://openenergymonitor.org/emon/node/1757 +// +const float phaseCal_grid = 1.0; // default value +const float phaseCal_diverted = 1.0; // default value + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // Energy Diversion Detection (for the local load only) +long requiredExportPerMainsCycle_inIEU; +float IEUtoJoulesConversion_CT1; + +void setup() +{ + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for the local dump-load + pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_2_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_3_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_4_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_5_pin, OUTPUT); // driver pin for an additional load + + for(int i = 0; i< noOfDumploads; i++) + { + logicalLoadState[i] = LOAD_OFF; + physicalLoadState[i] = LOAD_OFF; + } + + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // the local load is active low. + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // additional loads are active high. + digitalWrite(physicalLoad_2_pin, !physicalLoadState[2]); // additional loads are active high + digitalWrite(physicalLoad_3_pin, !physicalLoadState[3]); // additional loads are active high + digitalWrite(physicalLoad_4_pin, !physicalLoadState[4]); // additional loads are active high + digitalWrite(physicalLoad_5_pin, !physicalLoadState[5]); // additional loads are active high + + pinMode(loadPrioritySelectorPin, INPUT); // this pin is tracked to the "mode" connector + digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and + loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority + + delay(5000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_multiLoad_wired_6b.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, calibration values that are supplied in floating point + // form need to be rescaled. + // + phaseCal_grid_int = phaseCal_grid * 256; // for integer maths + phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); +// energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up + midPointOfEnergyBucket_long = capacityOfEnergyBucket_long / 2; + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the correct scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples. The main processor and +// the ADC work autonomously, their operation being only linked via the dataReady flag. +// As soon as a new set of data is made available by the ADC, the main processor can +// start to work on it immediately. +// +void allGeneralProcessing() +{ + static boolean beyondStartUpPhase = false; // start-up delay, allows things to settle + static long sumP_grid; // for per-cycle summation of 'real power' + static long sumP_diverted; // for per-cycle summation of 'real power' + static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) + static long lastSampleVminusDC_long; // for the phaseCal algorithm + static byte perSecondCounter = 0; + + // remove DC offset from the raw voltage sample by subtracting the accurate value + // as determined by a LP filter. + long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long; + + // determine the polarity of the latest voltage sample + if(sampleVminusDC_long > 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + // cycleCount++; <- this mechanism is unsafe, it will eventually overflow + + // a simple routine for checking the continuity of the sampling scheme + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / sampleSetsDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- for non-standard use + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Applying max and min limits to bucket's level has been + // deferred until after energy-related decisions have been taken. + + if (EDD_isActive) // Energy Diversion Display + { + // When locally diverted energy is being monitored, the latest contribution + // needs to be added to an accumulator which operates with maximum precision. + // + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) { + realEnergy_diverted = 0; } // to avoid 'creep' + + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + perSecondCounter++; + if(perSecondCounter >= CYCLES_PER_SECOND) + { + perSecondCounter = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); // occurs every second +/* + Serial.print (energyInBucket_long * IEUtoJoulesConversion_CT1); + Serial.print (", "); + for (byte loadID = 0; loadID < noOfDumploads; loadID++) + { + Serial.print (!physicalLoadState[loadID]); // 1 = "on", 0 = "off" + Serial.print (" "); + } + Serial.println(); +*/ +// Serial.println(activeLoad); + } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + // check to see whether the trigger device can now be reliably armed + if(sampleSetsDuringThisMainsCycle == 3) // should always exceed 20V (the min for trigger) + { + /* Determining whether any of the loads need to be changed is is a 3-stage process: + * - change the LOGICAL load states as necessary to maintain the energy level + * - update the PHYSICAL load states according to the logical -> physical mapping + * - update the driver lines for each of the loads. + */ + + // Restrictions apply for the period immediately after a load has been switched. + // Here the recentTransition flag is checked and updated as necessary. + if (recentTransition) + { + postTransitionCount++; + if (postTransitionCount >= POST_TRANSITION_MAX_COUNT) + { + recentTransition = false; + } + } + + if (energyInBucket_long > midPointOfEnergyBucket_long) + { + // the energy state is in the upper half of the working range + lowerEnergyThreshold = lowerThreshold_default; // reset the "opposite" threshold + if (energyInBucket_long > upperEnergyThreshold) + { + // Because the energy level is high, some action may be required + boolean OK_toAddLoad = true; + byte tempLoad = nextLogicalLoadToBeAdded(); + if (tempLoad < noOfDumploads) + { + // a load which is now OFF has been identified for potentially being switched ON + if (recentTransition) + { + // During the post-transition period, any increase in the energy level is noted. + if (energyInBucket_long > upperEnergyThreshold) + { + upperEnergyThreshold = energyInBucket_long; + + // the energy thresholds must remain within range + if (upperEnergyThreshold > capacityOfEnergyBucket_long) + { + upperEnergyThreshold = capacityOfEnergyBucket_long; + } + } + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toAddLoad = false; + } + } + + if (OK_toAddLoad) + { + logicalLoadState[tempLoad] = LOAD_ON; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + } + } + } + } + else + { // the energy state is in the lower half of the working range + upperEnergyThreshold = upperThreshold_default; // reset the "opposite" threshold + if (energyInBucket_long < lowerEnergyThreshold) + { + // Because the energy level is low, some action may be required + boolean OK_toRemoveLoad = true; + byte tempLoad = nextLogicalLoadToBeRemoved(); + if (tempLoad < noOfDumploads) + { + // a load which is now ON has been identified for potentially being switched OFF + if (recentTransition) + { + // During the post-transition period, any decrease in the energy level is noted. + if (energyInBucket_long < lowerEnergyThreshold) + { + lowerEnergyThreshold = energyInBucket_long; + + // the energy thresholds must remain within range + if (lowerEnergyThreshold < 0) + { + lowerEnergyThreshold = 0; + } + } + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toRemoveLoad = false; + } + } + + if (OK_toRemoveLoad) + { + logicalLoadState[tempLoad] = LOAD_OFF; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + } + } + } + } + + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + // update each of the physical loads + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // active high for additional load + digitalWrite(physicalLoad_2_pin, !physicalLoadState[2]); // active high for additional load + digitalWrite(physicalLoad_3_pin, !physicalLoadState[3]); // active high for additional load + digitalWrite(physicalLoad_4_pin, !physicalLoadState[4]); // active high for additional load + digitalWrite(physicalLoad_5_pin, !physicalLoadState[5]); // active high for additional load + + // update the Energy Diversion Detector + if (physicalLoadState[0] == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + + // Now that the energy-related decisions have been taken, min and max limits can now + // be applied to the level of the energy bucket. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + } + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > startUpPeriod * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringThisMainsCycle = 0; + sampleCount_forContinuityChecker = 0; + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + +// checkOutputModeSelection(); // updates outputMode if switch is changed + checkLoadPrioritySelection(); // updates load priorities if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is positive + + // processing for EVERY pair of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform + long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); +// long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform + long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); +// long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + sampleSetsDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + + refreshDisplay(); +} + + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count >= POLARITY_CHECK_MAXCOUNT) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + + +byte nextLogicalLoadToBeAdded() +{ + byte retVal = noOfDumploads; + boolean success = false; + for (byte index = 0; index < noOfDumploads && !success; index++) + { + if (logicalLoadState[index] == LOAD_OFF) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +byte nextLogicalLoadToBeRemoved() +{ + byte retVal = noOfDumploads; + boolean success = false; + + // this index counter can't be a 'byte' because the loop would run forever! + // + for (int index = (noOfDumploads -1); index >= 0 && !success; index--) + { + if (logicalLoadState[index] == LOAD_ON) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * The association between the physical and logical loads is 1:1. By default, numerical + * equivalence is maintained, so logical(N) maps to physical(N). If physical load 1 is set + * to have priority, rather than physical load 0, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * Any other mapping relaionships could be configured here. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } + + if (loadPriorityMode == LOAD_1_HAS_PRIORITY) + { + // swap physical loads 0 & 1 if remote load has priority + physicalLoadState[0] = logicalLoadState[1]; + physicalLoadState[1] = logicalLoadState[0]; + } +} + + +// this function changes the value of the load priorities if the state of the external switch is altered +void checkLoadPrioritySelection() +{ + static byte loadPrioritySwitchCount = 0; + int pinState = digitalRead(loadPrioritySelectorPin); + if (pinState != loadPriorityMode) + { + loadPrioritySwitchCount++; + } + if (loadPrioritySwitchCount >= 20) + { + loadPrioritySwitchCount = 0; + loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable + Serial.print ("loadPriority selection changed to "); + if (loadPriorityMode == LOAD_0_HAS_PRIORITY) { + Serial.println ( "load 0"); } + else { + Serial.println ( "load 1"); } + } +} + + +// Although this sketch always operates in ANTI_FLICKER mode, it was convenient +// to leave this mechanism in place. +// +void configureParamsForSelectedOutputMode() +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerThreshold_default = + capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperThreshold_default = + capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerThreshold_default = capacityOfEnergyBucket_long * 0.5; + upperThreshold_default = capacityOfEnergyBucket_long * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" lowerEnergyThreshold = "); + Serial.println(lowerThreshold_default); + Serial.print(" upperEnergyThreshold = "); + Serial.println(upperThreshold_default); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +// valueToBeDisplayed++; +} + + + + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_wired_7.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_wired_7.ino new file mode 100644 index 0000000..c1e3745 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_wired_7.ino @@ -0,0 +1,1291 @@ +/* Mk2_multiLoad_wired_7.ino + * + * This sketch is for diverting surplus PV power using multiple hard-wired loads. + * An external switch allows either load 0 or Load 1 to have the highest + * priority. Any number of loads can be supported by the logic, a dedicated + * IO pin being required for each one. + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The selector switch, as mentioned above, connects to the "mode" port which has been + * re-assigned for priority selection. "Normal" mode can be achieved by setting the + * anti-flicker offset prameter to zero at compile-time. + * + * The integral voltage sensor is fed from one of the secondary coils of the transformer. + * Current is measured via Current Transformers at the CT1 and CT2 ports. + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the 'diverted' current, so that energy which is diverted via the primary + * dump-load can be recorded and displayed locally. + * + * A persistence-based 4-digit display is supported. To free up the necessary IO pins + * for driving multiple loads, the pin-saving hardware needs to be in place. + * These extra logic chips (ICs 3 and 4) reduce the number of IO pins + * that are needed to drive the display. The freed-up pins are available at the + * J1-5 connector. The uppermost position has been assigned to drive Load 1, + * the next one down is for Load 2, and the lowest one is for Load 5. The control signal + * for Load 0 is available at the "trigger" connector. + * + * With the green (rev 2.1) version of my PCB, each of the additional outputs has an + * associated ground pin. It is therefore more sensible for those outputs to be active-high + * rather than active-low. With a 5V regulator rather than the normal 3.3V one, these + * outputs are able to drive an SSR directly. energymonitor.org/emon/node/1757 + * + * September 2014: renamed as Mk2_multiLoad_CAT5_3, with these changes: + * - reimplementation of cycleCount, as it could have overflowed with unpredictable results; + * - the functions increaseLoadIfPossible() and decreaseLoadIfPossible() have been tidied; + * - energyThreshold_long has been renamed as midPointOfEnergyBucket_long. + * + * December 2014: renamed as Mk2_multiLoad_CAT5_4, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed); + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances; + * - the logic for each of the 5 additional loads has been inverted by use of the '!' character. + * these outputs are now active-high rather than active-low + * + * November 2015: renamed as Mk2_multiLoad_wired_5, with these changes: + * - the original twin-threshold algorithm for energy state management has been reinstated; + * - improved mechanism for controlling multiple loads (faster and more accurate); + * - the phaseCal mechanism has been reinstated; + * - SWEETZONE_IN_JOULES has been replaced by WORKING_RANGE_IN_JOULES. + * + * January 2015: renamed as Mk2_multiLoad_wired_5a, with this change: + * - minor bug-fix in allGeneralProcessing() which affects how the energy thresholds are adjusted immediately + * after a change of load-state has tqaken place. + * + * January 2015: renamed as Mk2_multiLoad_wired_6, with this change: + * - reinstatement of min & max limits for the energy bucket's level. This section was lost during the + * conversion from version 4 to version 5. The absence of this section prevents diversion from starting + * in the correct manner. Versions 5 and 5a should therefore not be used. Version 6 is believed to be + * a correct implementation of the improved mechanism for controlling multiple loads. + * + * January 2016: renamed as Mk2_multiLoad_wired_6a, with a minor change in the ISR to + * remove a timing uncertainty. + * + * January 2016: updated to Mk2_multiLoad_wired_6b: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * February 2016: updated to Mk2_multiLoad_wired_7, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define WORKING_RANGE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +const byte noOfDumploads = 6; // The logic expects a minimum of 2 dumploads, + // for local & remote loads, but neither has to + // be physically present. + +// definitions of enumerated types and instances of same +enum polarities {NEGATIVE, POSITIVE}; +enum outputModes {ANTI_FLICKER, NORMAL}; +enum loadPriorityModes {LOAD_1_HAS_PRIORITY, LOAD_0_HAS_PRIORITY}; + +enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are active low +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +// ---------------- Extra Features selection ---------------------- +// +// - WORKLOAD_CHECK, for determining how much spare processing time there is. +// +// #define WORKLOAD_CHECK // <-- Include this line is this feature is required + + +// For most single-load Mk2 systems, the power-diversion logic can operate in either of two modes: +// +// - NORMAL, where the load switches rapidly on/off to maintain a constant energy level. +// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations +// of the local mains voltage. +// +// For this multi-load version, the same mechanism has been retained but the +// output mode is hard-coded as below: +enum outputModes outputMode = ANTI_FLICKER; + +// In this multi-load version, the external switch is re-used to determine the load priority +enum loadPriorityModes loadPriorityMode = LOAD_0_HAS_PRIORITY; + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +const byte physicalLoad_2_pin = 2; // <-- to control an additional load +const byte loadPrioritySelectorPin = 3; // <-- this is the "mode" port +const byte physicalLoad_0_pin = 4; // <-- this is the "trigger" port +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +const byte physicalLoad_3_pin = 10; // <-- to control an additional load +const byte physicalLoad_5_pin = 11; // <-- to control an additional load +const byte physicalLoad_1_pin = 12; // <-- to control an additional load +const byte physicalLoad_4_pin = 13; // <-- to control an additional load + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + +const byte delayBeforeSerialStarts = 3; // in seconds, to allow Serial window to be opened +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +long energyInBucket_long = 0; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long midPointOfEnergyBucket_long; // used for 'normal' and single-threshold 'AF' logic +int phaseCal_grid_int; // to avoid the need for floating-point maths +int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; + +long lowerThreshold_default; +long lowerEnergyThreshold; +long upperThreshold_default; +long upperEnergyThreshold; + +boolean recentTransition; +byte postTransitionCount; +#define POST_TRANSITION_MAX_COUNT 3 // <-- allows each transition to take effect +byte activeLoad = 0; + +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.4 + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +volatile int sampleI_grid; +volatile int sampleI_diverted; +volatile int sampleV; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define PERSISTENCE_FOR_POLARITY_CHANGE 2 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// With most hardware, the default values are likely to work fine without +// need for change. A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.044; // for CT2 + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When +// measuring a resistive load, the voltage and current waveforms should be perfectly +// aligned. In this situation, the Power Factor will be 1. +// +// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct +// value of phaseCal for any hardware configuration. An index of my various Mk2-related +// exhibits is available at http://openenergymonitor.org/emon/node/1757 +// +const float phaseCal_grid = 1.0; // default value +const float phaseCal_diverted = 1.0; // default value + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // Energy Diversion Detection (for the local load only) +long requiredExportPerMainsCycle_inIEU; +float IEUtoJoulesConversion_CT1; + +void setup() +{ + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for the local dump-load + pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_2_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_3_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_4_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_5_pin, OUTPUT); // driver pin for an additional load + + for(int i = 0; i< noOfDumploads; i++) + { + logicalLoadState[i] = LOAD_OFF; + physicalLoadState[i] = LOAD_OFF; + } + + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // the local load is active low. + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // additional loads are active high. + digitalWrite(physicalLoad_2_pin, !physicalLoadState[2]); // additional loads are active high + digitalWrite(physicalLoad_3_pin, !physicalLoadState[3]); // additional loads are active high + digitalWrite(physicalLoad_4_pin, !physicalLoadState[4]); // additional loads are active high + digitalWrite(physicalLoad_5_pin, !physicalLoadState[5]); // additional loads are active high + + pinMode(loadPrioritySelectorPin, INPUT); // this pin is tracked to the "mode" connector + digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and + loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority + + delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_multiLoad_wired_7.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, calibration values that are supplied in floating point + // form need to be rescaled. + // + phaseCal_grid_int = phaseCal_grid * 256; // for integer maths + phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + midPointOfEnergyBucket_long = capacityOfEnergyBucket_long / 2; + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the correct scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples. The main processor and +// the ADC work autonomously, their operation being only linked via the dataReady flag. +// As soon as a new set of data is made available by the ADC, the main processor can +// start to work on it immediately. +// +void allGeneralProcessing() +{ + static boolean beyondStartUpPhase = false; // start-up delay, allows things to settle + static long sumP_grid; // for per-cycle summation of 'real power' + static long sumP_diverted; // for per-cycle summation of 'real power' + static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) + static long lastSampleVminusDC_long; // for the phaseCal algorithm + static byte perSecondCounter = 0; + + // remove DC offset from the raw voltage sample by subtracting the accurate value + // as determined by a LP filter. + long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long; + + // determine the polarity of the latest voltage sample + if(sampleVminusDC_long > 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + if (beyondStartUpPhase) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + // cycleCount++; <- this mechanism is unsafe, it will eventually overflow + + // a simple routine for checking the continuity of the sampling scheme + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / sampleSetsDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- for non-standard use + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Applying max and min limits to bucket's level has been + // deferred until after energy-related decisions have been taken. + + if (EDD_isActive) // Energy Diversion Display + { + // When locally diverted energy is being monitored, the latest contribution + // needs to be added to an accumulator which operates with maximum precision. + // + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) { + realEnergy_diverted = 0; } // to avoid 'creep' + + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + perSecondCounter++; + if(perSecondCounter >= CYCLES_PER_SECOND) + { + perSecondCounter = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); // occurs every second +/* + Serial.print (energyInBucket_long * IEUtoJoulesConversion_CT1); + Serial.print (", "); + for (byte loadID = 0; loadID < noOfDumploads; loadID++) + { + Serial.print (!physicalLoadState[loadID]); // 1 = "on", 0 = "off" + Serial.print (" "); + } + Serial.println(); +*/ +// Serial.println(activeLoad); + } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringThisMainsCycle = 0; // not yet dealt with for this cycle + sampleCount_forContinuityChecker = 1; // opportunity has been missed for this cycle + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + // check to see whether the trigger device can now be reliably armed + if(sampleSetsDuringThisMainsCycle == 3) // should always exceed 20V (the min for trigger) + { + if (beyondStartUpPhase) + { + /* Determining whether any of the loads need to be changed is is a 3-stage process: + * - change the LOGICAL load states as necessary to maintain the energy level + * - update the PHYSICAL load states according to the logical -> physical mapping + * - update the driver lines for each of the loads. + */ + + // Restrictions apply for the period immediately after a load has been switched. + // Here the recentTransition flag is checked and updated as necessary. + if (recentTransition) + { + postTransitionCount++; + if (postTransitionCount >= POST_TRANSITION_MAX_COUNT) + { + recentTransition = false; + } + } + + if (energyInBucket_long > midPointOfEnergyBucket_long) + { + // the energy state is in the upper half of the working range + lowerEnergyThreshold = lowerThreshold_default; // reset the "opposite" threshold + if (energyInBucket_long > upperEnergyThreshold) + { + // Because the energy level is high, some action may be required + boolean OK_toAddLoad = true; + byte tempLoad = nextLogicalLoadToBeAdded(); + if (tempLoad < noOfDumploads) + { + // a load which is now OFF has been identified for potentially being switched ON + if (recentTransition) + { + // During the post-transition period, any increase in the energy level is noted. + if (energyInBucket_long > upperEnergyThreshold) + { + upperEnergyThreshold = energyInBucket_long; + + // the energy thresholds must remain within range + if (upperEnergyThreshold > capacityOfEnergyBucket_long) + { + upperEnergyThreshold = capacityOfEnergyBucket_long; + } + } + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toAddLoad = false; + } + } + + if (OK_toAddLoad) + { + logicalLoadState[tempLoad] = LOAD_ON; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + } + } + } + } + else + { // the energy state is in the lower half of the working range + upperEnergyThreshold = upperThreshold_default; // reset the "opposite" threshold + if (energyInBucket_long < lowerEnergyThreshold) + { + // Because the energy level is low, some action may be required + boolean OK_toRemoveLoad = true; + byte tempLoad = nextLogicalLoadToBeRemoved(); + if (tempLoad < noOfDumploads) + { + // a load which is now ON has been identified for potentially being switched OFF + if (recentTransition) + { + // During the post-transition period, any decrease in the energy level is noted. + if (energyInBucket_long < lowerEnergyThreshold) + { + lowerEnergyThreshold = energyInBucket_long; + + // the energy thresholds must remain within range + if (lowerEnergyThreshold < 0) + { + lowerEnergyThreshold = 0; + } + } + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toRemoveLoad = false; + } + } + + if (OK_toRemoveLoad) + { + logicalLoadState[tempLoad] = LOAD_OFF; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + } + } + } + } + + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + // update each of the physical loads + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // active high for additional load + digitalWrite(physicalLoad_2_pin, !physicalLoadState[2]); // active high for additional load + digitalWrite(physicalLoad_3_pin, !physicalLoadState[3]); // active high for additional load + digitalWrite(physicalLoad_4_pin, !physicalLoadState[4]); // active high for additional load + digitalWrite(physicalLoad_5_pin, !physicalLoadState[5]); // active high for additional load + + // update the Energy Diversion Detector + if (physicalLoadState[0] == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + + // Now that the energy-related decisions have been taken, min and max limits can now + // be applied to the level of the energy bucket. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12); + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + +// checkOutputModeSelection(); // updates outputMode if switch is changed + checkLoadPrioritySelection(); // updates load priorities if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is negative + + // processing for EVERY pair of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform + long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); +// long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform + long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); +// long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + sampleSetsDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + + refreshDisplay(); +} + + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count > PERSISTENCE_FOR_POLARITY_CHANGE) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + + +byte nextLogicalLoadToBeAdded() +{ + byte retVal = noOfDumploads; + boolean success = false; + for (byte index = 0; index < noOfDumploads && !success; index++) + { + if (logicalLoadState[index] == LOAD_OFF) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +byte nextLogicalLoadToBeRemoved() +{ + byte retVal = noOfDumploads; + boolean success = false; + + // this index counter can't be a 'byte' because the loop would run forever! + // + for (int index = (noOfDumploads -1); index >= 0 && !success; index--) + { + if (logicalLoadState[index] == LOAD_ON) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * The association between the physical and logical loads is 1:1. By default, numerical + * equivalence is maintained, so logical(N) maps to physical(N). If physical load 1 is set + * to have priority, rather than physical load 0, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * Any other mapping relaionships could be configured here. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } + + if (loadPriorityMode == LOAD_1_HAS_PRIORITY) + { + // swap physical loads 0 & 1 if remote load has priority + physicalLoadState[0] = logicalLoadState[1]; + physicalLoadState[1] = logicalLoadState[0]; + } +} + + +// this function changes the value of the load priorities if the state of the external switch is altered +void checkLoadPrioritySelection() +{ + static byte loadPrioritySwitchCount = 0; + int pinState = digitalRead(loadPrioritySelectorPin); + if (pinState != loadPriorityMode) + { + loadPrioritySwitchCount++; + } + if (loadPrioritySwitchCount >= 20) + { + loadPrioritySwitchCount = 0; + loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable + Serial.print ("loadPriority selection changed to "); + if (loadPriorityMode == LOAD_0_HAS_PRIORITY) { + Serial.println ( "load 0"); } + else { + Serial.println ( "load 1"); } + } +} + + +// Although this sketch always operates in ANTI_FLICKER mode, it was convenient +// to leave this mechanism in place. +// +void configureParamsForSelectedOutputMode() +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerThreshold_default = + capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperThreshold_default = + capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerThreshold_default = capacityOfEnergyBucket_long * 0.5; + upperThreshold_default = capacityOfEnergyBucket_long * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" lowerEnergyThreshold = "); + Serial.println(lowerThreshold_default); + Serial.print(" upperEnergyThreshold = "); + Serial.println(upperThreshold_default); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +// valueToBeDisplayed++; +} + + + + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_wired_7a.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_wired_7a.ino new file mode 100644 index 0000000..7eff073 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_multiLoad_wired_7a.ino @@ -0,0 +1,1289 @@ +/* Mk2_multiLoad_wired_7a.ino + * + * This sketch is for diverting suplus PV power using multiple hard-wired loads. + * An external switch allows either load 0 or Load 1 to have the highest + * priority. Any number of loads can be supported by the logic, a dedicated + * IO pin being required for each one. + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The selector switch, as mentioned above, connects to the "mode" port which has been + * re-assigned for priority selection. "Normal" mode can be achieved by setting the + * anti-flicker offset prameter to zero at compile-time. + * + * The integral voltage sensor is fed from one of the secondary coils of the transformer. + * Current is measured via Current Transformers at the CT1 and CT1 ports. + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the 'diverted' current, so that energy which is diverted via the primary + * dump-load can be recorded and displayed locally. + * + * A persistence-based 4-digit display is supported. To free up the necessary IO pins + * for driving multiple loads, the pin-saving hardware needs to be in place. + * These extra logic chips (ICs 3 and 4) reduce the number of IO pins + * that are needed to drive the display. The freed-up pins are available at the + * J1-5 connector. The uppermost position has been assigned to drive Load 1, + * the next one down is for Load 2, and the lowest one is for Load 5. The control signal + * for Load 0 is available at the "trigger" connector. + * + * With the green (rev 2.1) version of my PCB, each of the additional outputs has an + * associated ground pin. It is therefore more sensible for those outputs to be active-high + * rather than active-low. With a 5V regulator rather than the normal 3.3V one, these + * outputs are able to drive an SSR directly. energymonitor.org/emon/node/1757 + * + * September 2014: renamed as Mk2_multiLoad_CAT5_3, with these changes: + * - reimplementation of cycleCount, as it could have overflowed with unpredictable results; + * - the functions increaseLoadIfPossible() and decreaseLoadIfPossible() have been tidied; + * - energyThreshold_long has been renamed as midPointOfEnergyBucket_long. + * + * December 2014: renamed as Mk2_multiLoad_CAT5_4, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed); + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances; + * - the logic for each of the 5 additional loads has been inverted by use of the '!' character. + * these outputs are now active-high rather than active-low + * + * November 2015: renamed as Mk2_multiLoad_wired_5, with these changes: + * - the original twin-threshold algorithm for energy state management has been reinstated; + * - improved mechanism for controlling multiple loads (faster and more accurate); + * - the phaseCal mechanism has been reinstated; + * - SWEETZONE_IN_JOULES has been replaced by WORKING_RANGE_IN_JOULES. + * + * January 2015: renamed as Mk2_multiLoad_wired_5a, with this change: + * - minor bug-fix in allGeneralProcessing() which affects how the energy thresholds are adjusted immediately + * after a change of load-state has tqaken place. + * + * January 2015: renamed as Mk2_multiLoad_wired_6, with this change: + * - reinstatement of min & max limits for the energy bucket's level. This section was lost during the + * conversion from version 4 to version 5. The absence of this section prevents diversion from starting + * in the correct manner. Versions 5 and 5a should therefore not be used. Version 6 is believed to be + * a correct implementation of the improved mechanism for controlling multiple loads. + * + * January 2016: renamed as Mk2_multiLoad_wired_6a, with a minor change in the ISR to + * remove a timing uncertainty. + * + * January 2016: updated to Mk2_multiLoad_wired_6b: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * February 2016: updated to Mk2_multiLoad_wired_7, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * + * February 2020: updated to Mk2_multiLoad_wired_7a with these changes: + * - removal of some redundant code in the logic for determining the next load state. + * + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define WORKING_RANGE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +const byte noOfDumploads = 6; // The logic expects a minimum of 2 dumploads, + // for local & remote loads, but neither has to + // be physically present. + +// definitions of enumerated types and instances of same +enum polarities {NEGATIVE, POSITIVE}; +enum outputModes {ANTI_FLICKER, NORMAL}; +enum loadPriorityModes {LOAD_1_HAS_PRIORITY, LOAD_0_HAS_PRIORITY}; + +enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are active low +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +// ---------------- Extra Features selection ---------------------- +// +// - WORKLOAD_CHECK, for determining how much spare processing time there is. +// +// #define WORKLOAD_CHECK // <-- Include this line is this feature is required + + +// For most single-load Mk2 systems, the power-diversion logic can operate in either of two modes: +// +// - NORMAL, where the load switches rapidly on/off to maintain a constant energy level. +// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations +// of the local mains voltage. +// +// For this multi-load version, the same mechanism has been retained but the +// output mode is hard-coded as below: +enum outputModes outputMode = ANTI_FLICKER; + +// In this multi-load version, the external switch is re-used to determine the load priority +enum loadPriorityModes loadPriorityMode = LOAD_0_HAS_PRIORITY; + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +const byte physicalLoad_2_pin = 2; // <-- to control an additional load +const byte loadPrioritySelectorPin = 3; // <-- this is the "mode" port +const byte physicalLoad_0_pin = 4; // <-- this is the "trigger" port +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +const byte physicalLoad_3_pin = 10; // <-- to control an additional load +const byte physicalLoad_5_pin = 11; // <-- to control an additional load +const byte physicalLoad_1_pin = 12; // <-- to control an additional load +const byte physicalLoad_4_pin = 13; // <-- to control an additional load + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + +const byte delayBeforeSerialStarts = 3; // in seconds, to allow Serial window to be opened +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +long energyInBucket_long = 0; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long midPointOfEnergyBucket_long; // used for 'normal' and single-threshold 'AF' logic +int phaseCal_grid_int; // to avoid the need for floating-point maths +int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; + +long lowerThreshold_default; +long lowerEnergyThreshold; +long upperThreshold_default; +long upperEnergyThreshold; + +boolean recentTransition; +byte postTransitionCount; +#define POST_TRANSITION_MAX_COUNT 3 // <-- allows each transition to take effect +byte activeLoad = 0; + +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.4 + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +volatile int sampleI_grid; +volatile int sampleI_diverted; +volatile int sampleV; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define PERSISTENCE_FOR_POLARITY_CHANGE 2 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// With most hardware, the default values are likely to work fine without +// need for change. A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.044; // for CT2 + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When +// measuring a resistive load, the voltage and current waveforms should be perfectly +// aligned. In this situation, the Power Factor will be 1. +// +// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct +// value of phaseCal for any hardware configuration. An index of my various Mk2-related +// exhibits is available at http://openenergymonitor.org/emon/node/1757 +// +const float phaseCal_grid = 1.0; // default value +const float phaseCal_diverted = 1.0; // default value + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // Energy Diversion Detection (for the local load only) +long requiredExportPerMainsCycle_inIEU; +float IEUtoJoulesConversion_CT1; + +void setup() +{ + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for the local dump-load + pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_2_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_3_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_4_pin, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_5_pin, OUTPUT); // driver pin for an additional load + + for(int i = 0; i< noOfDumploads; i++) + { + logicalLoadState[i] = LOAD_OFF; + physicalLoadState[i] = LOAD_OFF; + } + + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // the local load is active low. + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // additional loads are active high. + digitalWrite(physicalLoad_2_pin, !physicalLoadState[2]); // additional loads are active high + digitalWrite(physicalLoad_3_pin, !physicalLoadState[3]); // additional loads are active high + digitalWrite(physicalLoad_4_pin, !physicalLoadState[4]); // additional loads are active high + digitalWrite(physicalLoad_5_pin, !physicalLoadState[5]); // additional loads are active high + + pinMode(loadPrioritySelectorPin, INPUT); // this pin is tracked to the "mode" connector + digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and + loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority + + delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_multiLoad_wired_7a.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, calibration values that are supplied in floating point + // form need to be rescaled. + // + phaseCal_grid_int = phaseCal_grid * 256; // for integer maths + phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + midPointOfEnergyBucket_long = capacityOfEnergyBucket_long / 2; + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the correct scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples. The main processor and +// the ADC work autonomously, their operation being only linked via the dataReady flag. +// As soon as a new set of data is made available by the ADC, the main processor can +// start to work on it immediately. +// +void allGeneralProcessing() +{ + static boolean beyondStartUpPhase = false; // start-up delay, allows things to settle + static long sumP_grid; // for per-cycle summation of 'real power' + static long sumP_diverted; // for per-cycle summation of 'real power' + static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) + static long lastSampleVminusDC_long; // for the phaseCal algorithm + static byte perSecondCounter = 0; + + // remove DC offset from the raw voltage sample by subtracting the accurate value + // as determined by a LP filter. + long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long; + + // determine the polarity of the latest voltage sample + if(sampleVminusDC_long > 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + if (beyondStartUpPhase) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + // cycleCount++; <- this mechanism is unsafe, it will eventually overflow + + // a simple routine for checking the continuity of the sampling scheme + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / sampleSetsDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- for non-standard use + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Applying max and min limits to bucket's level has been + // deferred until after energy-related decisions have been taken. + + if (EDD_isActive) // Energy Diversion Display + { + // When locally diverted energy is being monitored, the latest contribution + // needs to be added to an accumulator which operates with maximum precision. + // + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) { + realEnergy_diverted = 0; } // to avoid 'creep' + + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + perSecondCounter++; + if(perSecondCounter >= CYCLES_PER_SECOND) + { + perSecondCounter = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); // occurs every second +/* + Serial.print (energyInBucket_long * IEUtoJoulesConversion_CT1); + Serial.print (", "); + for (byte loadID = 0; loadID < noOfDumploads; loadID++) + { + Serial.print (!physicalLoadState[loadID]); // 1 = "on", 0 = "off" + Serial.print (" "); + } + Serial.println(); +*/ +// Serial.println(activeLoad); + } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringThisMainsCycle = 0; // not yet dealt with for this cycle + sampleCount_forContinuityChecker = 1; // opportunity has been missed for this cycle + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + // check to see whether the trigger device can now be reliably armed + if(sampleSetsDuringThisMainsCycle == 3) // should always exceed 20V (the min for trigger) + { + if (beyondStartUpPhase) + { + /* Determining whether any of the loads need to be changed is is a 3-stage process: + * - change the LOGICAL load states as necessary to maintain the energy level + * - update the PHYSICAL load states according to the logical -> physical mapping + * - update the driver lines for each of the loads. + */ + + // Restrictions apply for the period immediately after a load has been switched. + // Here the recentTransition flag is checked and updated as necessary. + if (recentTransition) + { + postTransitionCount++; + if (postTransitionCount >= POST_TRANSITION_MAX_COUNT) + { + recentTransition = false; + } + } + + if (energyInBucket_long > midPointOfEnergyBucket_long) + { + // the energy state is in the upper half of the working range + lowerEnergyThreshold = lowerThreshold_default; // reset the "opposite" threshold + if (energyInBucket_long > upperEnergyThreshold) + { + // Because the energy level is high, some action may be required + boolean OK_toAddLoad = true; + byte tempLoad = nextLogicalLoadToBeAdded(); + if (tempLoad < noOfDumploads) + { + // a load which is now OFF has been identified for potentially being switched ON + if (recentTransition) + { + // During the post-transition period, any increase in the energy level is noted. + upperEnergyThreshold = energyInBucket_long; + + // the energy thresholds must remain within range + if (upperEnergyThreshold > capacityOfEnergyBucket_long) + { + upperEnergyThreshold = capacityOfEnergyBucket_long; + } + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toAddLoad = false; + } + } + + if (OK_toAddLoad) + { + logicalLoadState[tempLoad] = LOAD_ON; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + } + } + } + } + else + { // the energy state is in the lower half of the working range + upperEnergyThreshold = upperThreshold_default; // reset the "opposite" threshold + if (energyInBucket_long < lowerEnergyThreshold) + { + // Because the energy level is low, some action may be required + boolean OK_toRemoveLoad = true; + byte tempLoad = nextLogicalLoadToBeRemoved(); + if (tempLoad < noOfDumploads) + { + // a load which is now ON has been identified for potentially being switched OFF + if (recentTransition) + { + // During the post-transition period, any decrease in the energy level is noted. + lowerEnergyThreshold = energyInBucket_long; + + // the energy thresholds must remain within range + if (lowerEnergyThreshold < 0) + { + lowerEnergyThreshold = 0; + } + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toRemoveLoad = false; + } + } + + if (OK_toRemoveLoad) + { + logicalLoadState[tempLoad] = LOAD_OFF; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + } + } + } + } + + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + // update each of the physical loads + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // active high for additional load + digitalWrite(physicalLoad_2_pin, !physicalLoadState[2]); // active high for additional load + digitalWrite(physicalLoad_3_pin, !physicalLoadState[3]); // active high for additional load + digitalWrite(physicalLoad_4_pin, !physicalLoadState[4]); // active high for additional load + digitalWrite(physicalLoad_5_pin, !physicalLoadState[5]); // active high for additional load + + // update the Energy Diversion Detector + if (physicalLoadState[0] == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + + // Now that the energy-related decisions have been taken, min and max limits can now + // be applied to the level of the energy bucket. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12); + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + +// checkOutputModeSelection(); // updates outputMode if switch is changed + checkLoadPrioritySelection(); // updates load priorities if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is negative + + // processing for EVERY pair of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform + long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); +// long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform + long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); +// long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + sampleSetsDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + + refreshDisplay(); +} + + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count > PERSISTENCE_FOR_POLARITY_CHANGE) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + + +byte nextLogicalLoadToBeAdded() +{ + byte retVal = noOfDumploads; + boolean success = false; + for (byte index = 0; index < noOfDumploads && !success; index++) + { + if (logicalLoadState[index] == LOAD_OFF) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +byte nextLogicalLoadToBeRemoved() +{ + byte retVal = noOfDumploads; + boolean success = false; + + // this index counter can't be a 'byte' because the loop would run forever! + // + for (int index = (noOfDumploads -1); index >= 0 && !success; index--) + { + if (logicalLoadState[index] == LOAD_ON) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * The association between the physical and logical loads is 1:1. By default, numerical + * equivalence is maintained, so logical(N) maps to physical(N). If physical load 1 is set + * to have priority, rather than physical load 0, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * Any other mapping relaionships could be configured here. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } + + if (loadPriorityMode == LOAD_1_HAS_PRIORITY) + { + // swap physical loads 0 & 1 if remote load has priority + physicalLoadState[0] = logicalLoadState[1]; + physicalLoadState[1] = logicalLoadState[0]; + } +} + + +// this function changes the value of the load priorities if the state of the external switch is altered +void checkLoadPrioritySelection() +{ + static byte loadPrioritySwitchCount = 0; + int pinState = digitalRead(loadPrioritySelectorPin); + if (pinState != loadPriorityMode) + { + loadPrioritySwitchCount++; + } + if (loadPrioritySwitchCount >= 20) + { + loadPrioritySwitchCount = 0; + loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable + Serial.print ("loadPriority selection changed to "); + if (loadPriorityMode == LOAD_0_HAS_PRIORITY) { + Serial.println ( "load 0"); } + else { + Serial.println ( "load 1"); } + } +} + + +// Although this sketch always operates in ANTI_FLICKER mode, it was convenient +// to leave this mechanism in place. +// +void configureParamsForSelectedOutputMode() +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerThreshold_default = + capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperThreshold_default = + capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerThreshold_default = capacityOfEnergyBucket_long * 0.5; + upperThreshold_default = capacityOfEnergyBucket_long * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" lowerEnergyThreshold = "); + Serial.println(lowerThreshold_default); + Serial.print(" upperEnergyThreshold = "); + Serial.println(upperThreshold_default); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +// valueToBeDisplayed++; +} + + + + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_standardDisplay_3loads_1.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_standardDisplay_3loads_1.ino new file mode 100644 index 0000000..2dc6d39 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_standardDisplay_3loads_1.ino @@ -0,0 +1,1116 @@ +/* Mk2_standardDisplay_3loads_1 + * + * This sketch is for diverting suplus PV power using multiple hard-wired loads. It is intended + * for use with my PCB-based hardware for the Mk2 PV Router. + * + * This sketch is only for use when the hardware is in its standard configuration with 14 wire links + * rather than including ICs 3 and 4. The control mode setting is hard-coded prior to compilation, + * options being NORMAL and ANTI_FLICKER. + * + * The integral voltage sensor is fed from one of the secondary coils of the transformer. + * Current is measured via Current Transformers at the CT1 and CT2 ports. + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the 'diverted' current, so that energy which is diverted via any of the dump-loads + * can be recorded and displayed locally. + * + * Up to three loads can be supported, the port allocation section has details of where the control signals + * can be accessed. + * + * (earlier history can be found in my multiLoad_wired sketches) + * + * February 2016: updated to Mk2_multiLoad_wired_7, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * + * February 2020: updated to Mk2_multiLoad_wired_7a with these changes: + * - removal of some redundant code in the logic for determining the next load state. + * + * April 2020: updated to Mk2_standardDisplay_3loads_1 with these changes: + * - change the display configuration to standard (i.e. 14 wire links, not pin-saving hardware) + * - provide 3 loads at ports D4, D3 and D15 (aka A1) + * - control mode is hard-coded to NORMAL, but ANTI_FLICKER mode is still available + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define WORKING_RANGE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +const byte noOfDumploads = 3; + +// definitions of enumerated types and instances of same +enum polarities {NEGATIVE, POSITIVE}; +enum outputModes {ANTI_FLICKER, NORMAL}; +enum loadPriorityModes {LOAD_1_HAS_PRIORITY, LOAD_0_HAS_PRIORITY}; + +enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are active low +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +// For most single-load Mk2 systems, the power-diversion logic can operate in either of two modes: +// +// - NORMAL, where the load switches rapidly on/off to maintain a constant energy level. +// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations +// of the local mains voltage. +// +// For this multi-load version, the same mechanism has been retained but the +// output mode is hard-coded as below: +enum outputModes outputMode = NORMAL; +// enum outputModes outputMode = ANTI_FLICKER; + +// In this multi-load version, an external switch is o available to determine the load priority +enum loadPriorityModes loadPriorityMode = LOAD_0_HAS_PRIORITY; + +// allocation of digital ports (with standard display hardware) +// ************************************************************ +// port D0 is for the serial inferface +// port D1 is for the serial inferface +// port D2 is used for the 4-digit display +const byte physicalLoad_1_port = 3; // <-- this port is active-high +const byte physicalLoad_0_port = 4; // <-- this port is active-low +// port D5 is used for the 4-digit display +// port D6 is used for the 4-digit display +// port D7 is used for the 4-digit display +// port D8 is used for the 4-digit display +// port D9 is used for the 4-digit display +// port D10 is used for the 4-digit display +// port D11 is used for the 4-digit display +// port D12 is used for the 4-digit display +// port D13 is used for the 4-digit display + +// allocation of analogue ports (with standard display hardware) +// ************************************************************* +// port D14 is used for the 4-digit display (also known as A0) +const byte physicalLoad_2_port = 15; // (also known as A1) +// port D16 is used for the 4-digit display (also known as A2) +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + +const byte delayBeforeSerialStarts = 3; // in seconds, to allow Serial window to be opened +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +long energyInBucket_long = 0; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long midPointOfEnergyBucket_long; // used for 'normal' and single-threshold 'AF' logic +int phaseCal_grid_int; // to avoid the need for floating-point maths +int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; + +long lowerThreshold_default; +long lowerEnergyThreshold; +long upperThreshold_default; +long upperEnergyThreshold; + +boolean recentTransition; +byte postTransitionCount; +#define POST_TRANSITION_MAX_COUNT 3 // <-- allows each transition to take effect +byte activeLoad = 0; + +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.4 + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +volatile int sampleI_grid; +volatile int sampleI_diverted; +volatile int sampleV; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define PERSISTENCE_FOR_POLARITY_CHANGE 2 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// With most hardware, the default values are likely to work fine without +// need for change. A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.044; // for CT2 + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When +// measuring a resistive load, the voltage and current waveforms should be perfectly +// aligned. In this situation, the Power Factor will be 1. +// +// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct +// value of phaseCal for any hardware configuration. An index of my various Mk2-related +// exhibits is available at http://openenergymonitor.org/emon/node/1757 +// +const float phaseCal_grid = 1.0; // default value +const float phaseCal_diverted = 1.0; // default value + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + + +// NB. This sketch is only intended for use with the standard display hardware that has +// 14 wire links rather than ICs 3 and 4 +// +#define ON HIGH +#define OFF LOW + +const byte noOfSegmentsPerDigit = 8; // includes one for the decimal point +enum digitEnableStates {DIGIT_ENABLED, DIGIT_DISABLED}; + +byte digitSelectorPin[noOfDigitLocations] = {16,10,13,11}; +byte segmentDrivePin[noOfSegmentsPerDigit] = {2,5,12,6,7,9,8,14}; + +// The final column of segMap[] is for the decimal point status. In this version, +// the decimal point is treated just like all the other segments, so there is +// no need to access this column specifically. +// +byte segMap[noOfPossibleCharacters][noOfSegmentsPerDigit] = { + ON , ON , ON , ON , ON , ON , OFF, OFF, // '0' <- element 0 + OFF, ON , ON , OFF, OFF, OFF, OFF, OFF, // '1' <- element 1 + ON , ON , OFF, ON , ON , OFF, ON , OFF, // '2' <- element 2 + ON , ON , ON , ON , OFF, OFF, ON , OFF, // '3' <- element 3 + OFF, ON , ON , OFF, OFF, ON , ON , OFF, // '4' <- element 4 + ON , OFF, ON , ON , OFF, ON , ON , OFF, // '5' <- element 5 + ON , OFF, ON , ON , ON , ON , ON , OFF, // '6' <- element 6 + ON , ON , ON , OFF, OFF, OFF, OFF, OFF, // '7' <- element 7 + ON , ON , ON , ON , ON , ON , ON , OFF, // '8' <- element 8 + ON , ON , ON , ON , OFF, ON , ON , OFF, // '9' <- element 9 + ON , ON , ON , ON , ON , ON , OFF, ON , // '0.' <- element 10 + OFF, ON , ON , OFF, OFF, OFF, OFF, ON , // '1.' <- element 11 + ON , ON , OFF, ON , ON , OFF, ON , ON , // '2.' <- element 12 + ON , ON , ON , ON , OFF, OFF, ON , ON , // '3.' <- element 13 + OFF, ON , ON , OFF, OFF, ON , ON , ON , // '4.' <- element 14 + ON , OFF, ON , ON , OFF, ON , ON , ON , // '5.' <- element 15 + ON , OFF, ON , ON , ON , ON , ON , ON , // '6.' <- element 16 + ON , ON , ON , OFF, OFF, OFF, OFF, ON , // '7.' <- element 17 + ON , ON , ON , ON , ON , ON , ON , ON , // '8.' <- element 18 + ON , ON , ON , ON , OFF, ON , ON , ON , // '9.' <- element 19 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, // ' ' <- element 20 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, ON // '.' <- element 11 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // Energy Diversion Detection (for the local load only) +long requiredExportPerMainsCycle_inIEU; +float IEUtoJoulesConversion_CT1; + +void setup() +{ + pinMode(physicalLoad_0_port, OUTPUT); // driver pin for the primary dump-load + pinMode(physicalLoad_1_port, OUTPUT); // driver pin for an additional load + pinMode(physicalLoad_2_port, OUTPUT); // driver pin for an additional load + + for(int i = 0; i< noOfDumploads; i++) + { + logicalLoadState[i] = LOAD_OFF; + physicalLoadState[i] = LOAD_OFF; + } + + digitalWrite(physicalLoad_0_port, physicalLoadState[0]); // the primary load is active low. + digitalWrite(physicalLoad_1_port, !physicalLoadState[1]); // additional loads are active high. + digitalWrite(physicalLoad_2_port, !physicalLoadState[2]); // additional loads are active high + + delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_standardDisplay_3loads_1.ino"); + Serial.println(); + +// NB. This sketch is only intended for use with the standard display hardware that has +// 14 wire links rather than ICs 3 and 4 +// + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + pinMode(segmentDrivePin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + pinMode(digitSelectorPin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + digitalWrite(digitSelectorPin[i], DIGIT_DISABLED); } + + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + digitalWrite(segmentDrivePin[i], OFF); } + + // When using integer maths, calibration values that are supplied in floating point + // form need to be rescaled. + // + phaseCal_grid_int = phaseCal_grid * 256; // for integer maths + phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + midPointOfEnergyBucket_long = capacityOfEnergyBucket_long / 2; + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the correct scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1< 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + if (beyondStartUpPhase) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + // cycleCount++; <- this mechanism is unsafe, it will eventually overflow + + // a simple routine for checking the continuity of the sampling scheme + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / sampleSetsDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- for non-standard use + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Applying max and min limits to bucket's level has been + // deferred until after energy-related decisions have been taken. + + if (EDD_isActive) // Energy Diversion Display + { + // When locally diverted energy is being monitored, the latest contribution + // needs to be added to an accumulator which operates with maximum precision. + // + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) { + realEnergy_diverted = 0; } // to avoid 'creep' + + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + perSecondCounter++; + if(perSecondCounter >= CYCLES_PER_SECOND) + { + perSecondCounter = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); // occurs every second + } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringThisMainsCycle = 0; // not yet dealt with for this cycle + sampleCount_forContinuityChecker = 1; // opportunity has been missed for this cycle + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + // check to see whether the trigger device can now be reliably armed + if(sampleSetsDuringThisMainsCycle == 3) // should always exceed 20V (the min for trigger) + { + if (beyondStartUpPhase) + { + /* Determining whether any of the loads need to be changed is is a 3-stage process: + * - change the LOGICAL load states as necessary to maintain the energy level + * - update the PHYSICAL load states according to the logical -> physical mapping + * - update the driver lines for each of the loads. + */ + + // Restrictions apply for the period immediately after a load has been switched. + // Here the recentTransition flag is checked and updated as necessary. + if (recentTransition) + { + postTransitionCount++; + if (postTransitionCount >= POST_TRANSITION_MAX_COUNT) + { + recentTransition = false; + } + } + + if (energyInBucket_long > midPointOfEnergyBucket_long) + { + // the energy state is in the upper half of the working range + lowerEnergyThreshold = lowerThreshold_default; // reset the "opposite" threshold + if (energyInBucket_long > upperEnergyThreshold) + { + // Because the energy level is high, some action may be required + boolean OK_toAddLoad = true; + byte tempLoad = nextLogicalLoadToBeAdded(); + if (tempLoad < noOfDumploads) + { + // a load which is now OFF has been identified for potentially being switched ON + if (recentTransition) + { + // During the post-transition period, any increase in the energy level is noted. + upperEnergyThreshold = energyInBucket_long; + + // the energy thresholds must remain within range + if (upperEnergyThreshold > capacityOfEnergyBucket_long) + { + upperEnergyThreshold = capacityOfEnergyBucket_long; + } + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toAddLoad = false; + } + } + + if (OK_toAddLoad) + { + logicalLoadState[tempLoad] = LOAD_ON; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + } + } + } + } + else + { // the energy state is in the lower half of the working range + upperEnergyThreshold = upperThreshold_default; // reset the "opposite" threshold + if (energyInBucket_long < lowerEnergyThreshold) + { + // Because the energy level is low, some action may be required + boolean OK_toRemoveLoad = true; + byte tempLoad = nextLogicalLoadToBeRemoved(); + if (tempLoad < noOfDumploads) + { + // a load which is now ON has been identified for potentially being switched OFF + if (recentTransition) + { + // During the post-transition period, any decrease in the energy level is noted. + lowerEnergyThreshold = energyInBucket_long; + + // the energy thresholds must remain within range + if (lowerEnergyThreshold < 0) + { + lowerEnergyThreshold = 0; + } + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toRemoveLoad = false; + } + } + + if (OK_toRemoveLoad) + { + logicalLoadState[tempLoad] = LOAD_OFF; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + } + } + } + } + + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + // update each of the physical loads + digitalWrite(physicalLoad_0_port, physicalLoadState[0]); // active low for he primary load + digitalWrite(physicalLoad_1_port, !physicalLoadState[1]); // active high for additional load + digitalWrite(physicalLoad_2_port, !physicalLoadState[2]); // active high for additional load + + // update the Energy Diversion Detector + if (physicalLoadState[0] == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + + // Now that the energy-related decisions have been taken, min and max limits can now + // be applied to the level of the energy bucket. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12); + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + +// checkOutputModeSelection(); // updates outputMode if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is negative + + // processing for EVERY pair of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform + long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); +// long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform + long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); +// long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + sampleSetsDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + + refreshDisplay(); +} + + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count > PERSISTENCE_FOR_POLARITY_CHANGE) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + + +byte nextLogicalLoadToBeAdded() +{ + byte retVal = noOfDumploads; + boolean success = false; + for (byte index = 0; index < noOfDumploads && !success; index++) + { + if (logicalLoadState[index] == LOAD_OFF) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +byte nextLogicalLoadToBeRemoved() +{ + byte retVal = noOfDumploads; + boolean success = false; + + // this index counter can't be a 'byte' because the loop would run forever! + // + for (int index = (noOfDumploads -1); index >= 0 && !success; index--) + { + if (logicalLoadState[index] == LOAD_ON) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * The association between the physical and logical loads is 1:1. By default, numerical + * equivalence is maintained, so logical(N) maps to physical(N). If physical load 1 is set + * to have priority, rather than physical load 0, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * Any other mapping relaionships could be configured here. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } + + if (loadPriorityMode == LOAD_1_HAS_PRIORITY) + { + // swap physical loads 0 & 1 if remote load has priority + physicalLoadState[0] = logicalLoadState[1]; + physicalLoadState[1] = logicalLoadState[0]; + } +} + + +// Although this sketch always operates in a hard-coded mode, it was convenient +// to leave this mechanism in place. +// +void configureParamsForSelectedOutputMode() +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerThreshold_default = + capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperThreshold_default = + capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerThreshold_default = capacityOfEnergyBucket_long * 0.5; + upperThreshold_default = capacityOfEnergyBucket_long * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" lowerEnergyThreshold = "); + Serial.println(lowerThreshold_default); + Serial.print(" upperEnergyThreshold = "); + Serial.println(upperThreshold_default); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +// valueToBeDisplayed++; +} + + + + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + // The two versions of the hardware require different logic. + +// NB. This sketch is only intended for use with the standard display hardware that has +// 14 wire links rather than ICs 3 and 4 +// + + // This version is more straightforward because the digit-enable lines can be + // used to mask out all of the transitory states, including the Decimal Point. + // The sequence is: + // + // 1. de-activate the digit-enable line that was previously active + // 2. determine the next location which is to be active + // 3. determine the relevant character for the new active location + // 4. set up the segment drivers for the character to be displayed (includes the DP) + // 5. activate the digit-enable line for the new active location + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + displayTime_count = 0; + + // 1. de-activate the location which is currently being displayed + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_DISABLED); + + // 2. determine the next digit location which is to be displayed + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 3. determine the relevant character for the new active location + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 4. set up the segment drivers for the character to be displayed (includes the DP) + for (byte segment = 0; segment < noOfSegmentsPerDigit; segment++) { + byte segmentState = segMap[digitVal][segment]; + digitalWrite( segmentDrivePin[segment], segmentState); } + + // 5. activate the digit-enable line for the new active location + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_ENABLED); + } + +} // end of refreshDisplay() + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_withRemoteLoad_1.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_withRemoteLoad_1.ino new file mode 100644 index 0000000..2e53217 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_withRemoteLoad_1.ino @@ -0,0 +1,1254 @@ +/* Mk2_RFremoteLoad_1.ino + * + * This sketch is for diverting surplus PV power to a local dump load using a triac. + * A remote load is also supported, this being controlled using the on-board + * RFM12B module. A switch is also supported which allows either load to be + * selected as having thei higher priority. If a local load is not provided, + * all surplus power is available for use by the remote load. + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The selector switch, as mentioned above, connects to the "mode" port. For this + * version of the Mk2 code, the system uses an alternative version of the anti-flicker + * algorithm which is well suited for multiple loads. As the 'normal' mode is no longer + * required, the 'mode' port can be re-assigned for priority selection. + * + * The integral voltage sensor is fed from one of the secondary coils of the transformer. + * Current is measured via Current Transformers at the CT1 and CT2 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the 'diverted' current, so that energy which is diverted via the + * local dump load can be recorded and displayed locally. + * + * A persistence-based 4-digit display is supported. When the RFM12B module is + * in use, the display can only be used in conjunction with an extra pair of + * logic chips. These are ICs 3 and 4, which reduce the number of processor pins + * that are needed to drive the display. + * + * This sketch has many similarities with Revision 5c of the Mk2i PV Router code that I + * have posted on the OpenEnergyMonitor forum. That version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * Robin Emley + * www.Mk2PVrouter.co.uk + * April 2014 + */ + +#include +#include // JeeLib is available at from: http://github.com/jcw/jeelib +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define SWEETZONE_IN_JOULES 3600 +#define REFRESH_PERIOD_IN_CYCLES 50 // max allowed interval between RF messages + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +const byte noOfDumploads = 2; // The logic expects a minimum of 2 dumploads, + // for local & remote loads, but neither has to + // be physically present. + +// definitions of enumerated types and instances of same +enum polarities {NEGATIVE, POSITIVE}; +enum outputModes {ANTI_FLICKER, NORMAL}; +enum loadPriorityModes {REMOTE_HAS_PRIORITY, LOCAL_HAS_PRIORITY}; + +enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are active low +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +enum energyStates {LOWER_HALF, UPPER_HALF}; // for single threshold AF algorithm +enum energyStates energyStateNow; + +// ---------------- Extra Features selection ---------------------- +// +// - WORKLOAD_CHECK, for determining how much spare processing time there is. +// +// #define WORKLOAD_CHECK // <-- Include this line is this feature is required + + +// For most single-load Mk2 systems, the power-diversion logic can operate in either of two modes: +// +// - NORMAL, where the triac switches rapidly on/off to maintain a constant energy level. +// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations +// of the local mains voltage. +// +// For this multi-load version, the same mechanism has been retained but the +// output mode is hard-coded as below: +enum outputModes outputMode = ANTI_FLICKER; + +// In this multi-load version, the external switch is re-used to determine the load priority +enum loadPriorityModes loadPriorityMode = LOCAL_HAS_PRIORITY; + +/* frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ + */ +#define freq RF12_868MHZ // Use the freq to match the module you have. + +const int nodeID = 10; // RFM12B node ID +const int networkGroup = 210; // RFM12B wireless network group - needs to be same as emonBase and emonGLCD +const int UNO = 1; // Set to 0 if you're not using the UNO bootloader (i.e using Duemilanove) + // - All Atmega's shipped from OpenEnergyMonitor come with Arduino Uno bootloader + +const int noOfCyclesBeforeRefresh = 50; // nominally every second, but not critical +long cycleCountAtLastRFtransmission = 0; +int messageNumber = 0; + +// data structure for RF comms +typedef struct { byte dumpState; int msgNumber; } Tx_struct; // data for RF comms +Tx_struct tx_data; + + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +// D2 is for the RFM12B +const byte loadPrioritySelectorPin = 3; // <-- this is the "mode" port +const byte physicalLoad_0_pin = 4; // <-- this is the "trigger" port +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +// D10 is for the RFM12B +// D11 is for the RFM12B +// D12 is for the RFM12B +// D13 is for the RFM12B + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + + +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +long cycleCount = 0; // counts mains cycles from start-up +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long energyThreshold_long; // used for 'normal' and single-threshold 'AF' logic +// int phaseCal_grid_int; // to avoid the need for floating-point maths +// int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; + +int postMidPointCrossingDelay_cycles; // assigned in setup(), different for each output mode +const int postMidPointCrossingDelayForAF_cycles = 25; // in 20 ms counts +const int interLoadSeparationDelay_cycles = 25; // in 20 ms cycle counts (for both output modes) +byte activeLoadID; // only one load may operate freely at a time. +long cycleCountAtLastMidPointCrossing = 0; +long cycleCountAtLastTransition = 0; +long energyAtLastOffTransition_long; +long energyAtLastOnTransition_long; + + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +int sampleI_grid; +int sampleI_diverted; +int sampleV; + +// for the remote load that is controlled by RF +unsigned long cycleCountAtLastTransmission = 0; +boolean sendRFcommandNextTime = false; + + + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// With most hardware, the default values are likely to work fine without +// need for change. A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When +// measuring a resistive load, the voltage and current waveforms should be perfectly +// aligned. In this situation, the Power Factor will be 1. +// +// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct +// value of phaseCal for any hardware configuration. An index of my various Mk2-related +// exhibits is available at http://openenergymonitor.org/emon/node/1757 +// +//const float phaseCal_grid = 1.0; <--- not used in this version +//const float phaseCal_diverted = 1.0; <--- not used in this version + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 10 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // Energy Diversion Detection (for the local load only) + + +void setup() +{ + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for the local dump-load + for(int i = 0; i< noOfDumploads; i++) + { + logicalLoadState[i] = LOAD_OFF; + physicalLoadState[i] = LOAD_OFF; + } + + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // force the local load to be in the "off" state. + + pinMode(loadPrioritySelectorPin, INPUT); // this pin is tracked to the "mode" connector + digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and + loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority + + delay(5000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_withRemoteLoad_1.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, calibration values that are supplied in floating point + // form need to be rescaled. + // +// phaseCal_grid_int = phaseCal_grid * 256; // for integer maths <-- not supported +// phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths <-- not supported + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the correct scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples. The main processor and +// the ADC work autonomously, their operation being only linked via the dataReady flag. +// As soon as a new set of data is made available by the ADC, the main processor can +// start to work on it immediately. +// +void allGeneralProcessing() +{ + static boolean beyondStartUpPhase = false; // start-up delay, allows things to settle + static boolean triggerNeedsToBeArmed = false; // once per mains cycle (+ve half) + static int samplesDuringThisMainsCycle; // for normalising the power in each mains cycle + static long sumP_grid; // for per-cycle summation of 'real power' + static long sumP_diverted; // for per-cycle summation of 'real power' + static enum polarities polarityOfLastSampleV; // for zero-crossing detection + static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) + static long lastSampleVminusDC_long; // for the phaseCal algorithm + static byte perSecondCounter = 0; + + // remove DC offset from the raw voltage sample by subtracting the accurate value + // as determined by a LP filter. + long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long; + + // determine the polarity of the latest voltage sample + enum polarities polarityNow; + if(sampleVminusDC_long > 0) { + polarityNow = POSITIVE; } + else { + polarityNow = NEGATIVE; } + + if (polarityNow == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + cycleCount++; + + triggerNeedsToBeArmed = true; // the trigger is armed once during each +ve half-cycle + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / samplesDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / samplesDuringThisMainsCycle; // proportional to Watts + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + if (EDD_isActive) // Energy Diversion Display + { + // When locally diverted energy is being monitored, the latest contribution + // needs to be added to an accumulator which operates with maximum precision. + // + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) { + realEnergy_diverted = 0; } // to avoid 'creep' + + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + perSecondCounter++; + if(perSecondCounter >= CYCLES_PER_SECOND) + { + perSecondCounter = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); // occurs every second + } + + + // when using the single-threshold power diversion algorithm, a counter needs + // to be reset whenever the energy level in the accumulator crosses the mid-point + // + enum energyStates energyStateOnLastLoop = energyStateNow; + + if (energyInBucket_long > energyThreshold_long) { + energyStateNow = UPPER_HALF; } + else { + energyStateNow = LOWER_HALF; } + + if (energyStateNow != energyStateOnLastLoop) { + cycleCountAtLastMidPointCrossing = cycleCount; } + + + // clear the per-cycle accumulators for use in this new mains cycle. + samplesDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + if (triggerNeedsToBeArmed == true) + { + // check to see whether the trigger device can now be reliably armed + if(samplesDuringThisMainsCycle == 3) // should always exceed 20V (the min for trigger) + { + boolean OKtoSendRFcommandNow = false; + boolean changeOfLoadState = false; + + // a pending RF command takes priority over the normal logic + if (sendRFcommandNextTime) + { + OKtoSendRFcommandNow = true; + sendRFcommandNextTime = false; + } + else + { + /* Now it's time to determine whether any of the the loads need to be changed. + * This is a 2-stage process: + * First, change the LOGICAL loads as necessary, then update the PHYSICAL + * loads according to the mapping that exists between them. The mapping is + * 1:1 by default but can be altered by a hardware switch which allows the + * priority of the remote load to be altered. + * This code uses a single-threshold algorithm which relies on regular switching + * of the load. + */ + if (cycleCount > cycleCountAtLastMidPointCrossing + postMidPointCrossingDelay_cycles) + { + if (energyInBucket_long > energyThreshold_long) + { + increaseLoadIfPossible(); // to reduce the level in the bucket + } + else + { + decreaseLoadIfPossible(); // to increase the level in the bucket + } + } + + + /* Update the state of all physical loads and determine whether the + * state of the remote load has changed. The remote load is hard-coded + * as Physical Load 1 (Physical Load 0 is a local load) + */ + boolean remoteLoadHasChangedState = false; + byte prevStateOfRemoteLoad = (byte)physicalLoadState[1]; + updatePhysicalLoadStates(); + if ((byte)physicalLoadState[1] != prevStateOfRemoteLoad) + { + remoteLoadHasChangedState = true; + } + + /* Now determine whether an RF command should be sent. This can be for + * either of two reasons: + * + * - the on/off state of the remote load needs to be changed + * - a refresh command is due ('cos no change of state has occurred recently) + * + * If the on/off state needs to be changed, but a refresh command was sent on the + * previous cycle, the 'change of state' command is deferred until the next + * cycle. A refresh command can always be sent straight away because it can + * be guaranteed that no command has been sent immediately beforehand. + */ + + // determine how long it's been since the last RF command was sent + long cycleCountSinceLastRFtransmission = cycleCount - cycleCountAtLastRFtransmission; + + if (remoteLoadHasChangedState) + { + // ensure that RF commands are not sent on consecutive cycles + if (cycleCountSinceLastRFtransmission > 1) + { + // the "change of state" can be acted on immediately + OKtoSendRFcommandNow = true; // local flag + } + else + { + // the "change of state" must be deferred until next cycle + sendRFcommandNextTime = true; // global flag + } + } + else + { + // no "change of state", so check whether a refresh command is due + if (cycleCountSinceLastRFtransmission >= noOfCyclesBeforeRefresh) + { + // a refresh command is due (which can always be sent immediately) + OKtoSendRFcommandNow = true; + } + } + } + + // update the local load, which is physical load 0 + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); + + // update the remote load, which is physical load 1 + if (OKtoSendRFcommandNow) + { + cycleCountAtLastRFtransmission = cycleCount; + tx_data.msgNumber = messageNumber++; + tx_data.dumpState = physicalLoadState[1]; +// tx_data.dumpState = messageNumber & 1; // for test only + send_rf_data(); + /* + Serial.print(tx_data.dumpState); + Serial.print(", "); + Serial.print(tx_data.msgNumber); + Serial.print("; "); + Serial.println(activeLoadID); // useful for keeping track of different priority loads + */ + } + + // update the Energy Diversion Detector + if (physicalLoadState[0] == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + + // clear the flag which ensures that loads are only updated once per mains cycle + triggerNeedsToBeArmed = false; + } + } + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > startUpPeriod * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + samplesDuringThisMainsCycle = 0; + Serial.println ("Go!"); + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + +// checkOutputModeSelection(); // updates outputMode if switch is changed + checkLoadPrioritySelection(); // updates load priorities if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is positive + + // processing for EVERY pair of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform +// long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform +// long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + samplesDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityOfLastSampleV = polarityNow; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + + +void increaseLoadIfPossible() +{ + /* if permitted by A/F rules, turn on the highest priority logical load that is not already on. + */ + boolean changed = false; + + // Only one load may operate freely at a time. Other loads are prevented from + // switching until a sufficient period has elapsed since the last transition, and + // then only if the energy level has not have fallen since the previous ON transition. + // These measures allow a lower priority load to contribute if a higher priority + // load is not having the desired effect, but not immediately. + // + if (energyInBucket_long >= energyAtLastOnTransition_long) + { + boolean timeout = (cycleCount > cycleCountAtLastTransition + interLoadSeparationDelay_cycles); + for (int i = 0; i < noOfDumploads && !changed; i++) + { + if (logicalLoadState[i] == LOAD_OFF) + { + if ((i == activeLoadID) || timeout) + { + logicalLoadState[i] = LOAD_ON; + cycleCountAtLastTransition = cycleCount; + energyAtLastOnTransition_long = energyInBucket_long; +// energyAtLastOffTransition_long = 3600; // reset the 'opposite' mechanism. <-WRONG! + energyAtLastOffTransition_long = capacityOfEnergyBucket_long; // reset the 'opposite' mechanism. + activeLoadID = i; + changed = true; + } + } + } + } + else + { + // energy level has not risen so there's no need to apply any more load + } +// return (changed); +} + +void decreaseLoadIfPossible() +{ + /* if permitted by A/F rules, turn off the highest priority logical load that is not already off. + */ + boolean changed = false; + + // Only one load may operate freely at a time. Other loads are prevented from + // switching until a sufficient period has elapsed since the last transition, and + // then only if the energy level has not have risen since the previous OFF transition. + // These measures allow a lower priority load to contribute if a higher priority + // load is not having the desired effect, but not immediately. + // + if (energyInBucket_long <= energyAtLastOnTransition_long) + { + boolean timeout = (cycleCount > cycleCountAtLastTransition + interLoadSeparationDelay_cycles); +// for (int i = 0; i < noOfDumploads && !done; i++) + for (int i = (noOfDumploads -1); i >= 0 && !changed; i--) + { + if (logicalLoadState[i] == LOAD_ON) + { + if ((i == activeLoadID) || timeout) + { + logicalLoadState[i] = LOAD_OFF; + cycleCountAtLastTransition = cycleCount; + energyAtLastOnTransition_long = energyInBucket_long; + energyAtLastOffTransition_long = 0; // reset the 'opposite' mechanism. + activeLoadID = i; + changed = true; + } + } + } + } + else + { + // energy level has not fallen so there's no need to apply any more load + } +// return (changed); +} + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * By default, the association between the physical and logical loads is 1:1. If + * the remote load is set to have priority, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * - Physical load 0 is local, and is controlled by use of an output pin. + * - Physical load 1 is remote, and is controlled via the associated RFM12B module. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } + + if (loadPriorityMode == REMOTE_HAS_PRIORITY) + { + // swap physical loads 0 & 1 if remote load has priority + physicalLoadState[0] = logicalLoadState[1]; + physicalLoadState[1] = logicalLoadState[0]; + } +} + + +// this function changes the value of the load priorities if the state of the external switch is altered +void checkLoadPrioritySelection() +{ + static byte loadPrioritySwitchCcount = 0; + int pinState = digitalRead(loadPrioritySelectorPin); + if (pinState != loadPriorityMode) + { + loadPrioritySwitchCcount++; + } + if (loadPrioritySwitchCcount >= 20) + { + loadPrioritySwitchCcount = 0; + loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable + Serial.print ("loadPriority selection changed to "); + if (loadPriorityMode == LOCAL_HAS_PRIORITY) { + Serial.println ( "local"); } + else { + Serial.println ( "remote"); } + } +} + + +// Although this sketch always operates in ANTI_FLICKER mode, it was convenient +// to leave this mechanism in place. +// +void configureParamsForSelectedOutputMode() +{ + energyThreshold_long = capacityOfEnergyBucket_long * 0.5; + + if (outputMode == ANTI_FLICKER) + { + postMidPointCrossingDelay_cycles = postMidPointCrossingDelayForAF_cycles; + } + else + { + postMidPointCrossingDelay_cycles = 0; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" energyThreshold_long = "); + Serial.println(energyThreshold_long); + Serial.print(" postMidPointCrossingDelay_cycles = "); + Serial.println(postMidPointCrossingDelay_cycles); + Serial.print(" interLoadSeparationDelay_cycles = "); + Serial.println(interLoadSeparationDelay_cycles); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +// valueToBeDisplayed++; +} + + + + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + +void send_rf_data() +{ + rf12_sleep(RF12_WAKEUP); + // if ready to send + exit route if it gets stuck + int i = 0; + while (!rf12_canSend() && i<10) + { + rf12_recvDone(); + i++; + } + rf12_sendStart(0, &tx_data, sizeof tx_data); + rf12_sendWait(2); + rf12_sleep(RF12_SLEEP); +} + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_withRemoteLoad_2.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_withRemoteLoad_2.ino new file mode 100644 index 0000000..1ff12cb --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_withRemoteLoad_2.ino @@ -0,0 +1,1267 @@ +/* Mk2_RFremoteLoad_2.ino + * + * This sketch is for diverting surplus PV power to a local dump load using a triac. + * A remote load is also supported, this being controlled using the on-board + * RFM12B module. A switch is also supported which allows either load to be + * selected as having thei higher priority. If a local load is not provided, + * all surplus power is available for use by the remote load. + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The selector switch, as mentioned above, connects to the "mode" port. For this + * version of the Mk2 code, the system uses an alternative version of the anti-flicker + * algorithm which is well suited for multiple loads. As the 'normal' mode is no longer + * required, the 'mode' port can be re-assigned for priority selection. + * + * The integral voltage sensor is fed from one of the secondary coils of the transformer. + * Current is measured via Current Transformers at the CT1 and CT2 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the 'diverted' current, so that energy which is diverted via the + * local dump load can be recorded and displayed locally. + * + * A persistence-based 4-digit display is supported. When the RFM12B module is + * in use, the display can only be used in conjunction with an extra pair of + * logic chips. These are ICs 3 and 4, which reduce the number of processor pins + * that are needed to drive the display. + * + * This sketch has many similarities with Revision 5c of the Mk2i PV Router code that I + * have posted on the OpenEnergyMonitor forum. That version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * Robin Emley + * www.Mk2PVrouter.co.uk + * April 2014 + */ + +#include +#include // JeeLib is available at from: http://github.com/jcw/jeelib +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define SWEETZONE_IN_JOULES 3600 +#define REFRESH_PERIOD_IN_CYCLES 50 // max allowed interval between RF messages +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +const byte noOfDumploads = 2; // The logic expects a minimum of 2 dumploads, + // for local & remote loads, but neither has to + // be physically present. + +// definitions of enumerated types and instances of same +enum polarities {NEGATIVE, POSITIVE}; +enum outputModes {ANTI_FLICKER, NORMAL}; +enum loadPriorityModes {REMOTE_HAS_PRIORITY, LOCAL_HAS_PRIORITY}; + +enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are active low +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +enum energyStates {LOWER_HALF, UPPER_HALF}; // for single threshold AF algorithm +enum energyStates energyStateNow; + +// ---------------- Extra Features selection ---------------------- +// +// - WORKLOAD_CHECK, for determining how much spare processing time there is. +// +// #define WORKLOAD_CHECK // <-- Include this line is this feature is required + + +// For most single-load Mk2 systems, the power-diversion logic can operate in either of two modes: +// +// - NORMAL, where the triac switches rapidly on/off to maintain a constant energy level. +// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations +// of the local mains voltage. +// +// For this multi-load version, the same mechanism has been retained but the +// output mode is hard-coded as below: +enum outputModes outputMode = ANTI_FLICKER; + +// In this multi-load version, the external switch is re-used to determine the load priority +enum loadPriorityModes loadPriorityMode = LOCAL_HAS_PRIORITY; + +/* frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ + */ +#define freq RF12_868MHZ // Use the freq to match the module you have. + +const int nodeID = 10; // RFM12B node ID +const int networkGroup = 210; // RFM12B wireless network group - needs to be same as emonBase and emonGLCD +const int UNO = 1; // Set to 0 if you're not using the UNO bootloader (i.e using Duemilanove) + // - All Atmega's shipped from OpenEnergyMonitor come with Arduino Uno bootloader + +const int noOfCyclesBeforeRefresh = 50; // nominally every second, but not critical +long cycleCountAtLastRFtransmission = 0; +int messageNumber = 0; + +// data structure for RF comms +typedef struct { byte dumpState; int msgNumber; } Tx_struct; // data for RF comms +Tx_struct tx_data; + + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +// D2 is for the RFM12B +const byte loadPrioritySelectorPin = 3; // <-- this is the "mode" port +const byte physicalLoad_0_pin = 4; // <-- this is the "trigger" port +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +// D10 is for the RFM12B +// D11 is for the RFM12B +// D12 is for the RFM12B +// D13 is for the RFM12B + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + + +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +long cycleCount = 0; // counts mains cycles from start-up +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long energyThreshold_long; // used for 'normal' and single-threshold 'AF' logic +// int phaseCal_grid_int; // to avoid the need for floating-point maths +// int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; + +int postMidPointCrossingDelay_cycles; // assigned in setup(), different for each output mode +const int postMidPointCrossingDelayForAF_cycles = 25; // in 20 ms counts +const int interLoadSeparationDelay_cycles = 25; // in 20 ms cycle counts (for both output modes) +byte activeLoadID; // only one load may operate freely at a time. +long cycleCountAtLastMidPointCrossing = 0; +long cycleCountAtLastTransition = 0; +long energyAtLastOffTransition_long; +long energyAtLastOnTransition_long; + + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +int sampleI_grid; +int sampleI_diverted; +int sampleV; + +// for the remote load that is controlled by RF +unsigned long cycleCountAtLastTransmission = 0; +boolean sendRFcommandNextTime = false; + + + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// With most hardware, the default values are likely to work fine without +// need for change. A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When +// measuring a resistive load, the voltage and current waveforms should be perfectly +// aligned. In this situation, the Power Factor will be 1. +// +// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct +// value of phaseCal for any hardware configuration. An index of my various Mk2-related +// exhibits is available at http://openenergymonitor.org/emon/node/1757 +// +//const float phaseCal_grid = 1.0; <--- not used in this version +//const float phaseCal_diverted = 1.0; <--- not used in this version + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 10 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // Energy Diversion Detection (for the local load only) +long requiredExportPerMainsCycle_inIEU; +float IEUtoJoulesConversion_CT1; + + +void setup() +{ + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for the local dump-load + for(int i = 0; i< noOfDumploads; i++) + { + logicalLoadState[i] = LOAD_OFF; + physicalLoadState[i] = LOAD_OFF; + } + + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // force the local load to be in the "off" state. + + pinMode(loadPrioritySelectorPin, INPUT); // this pin is tracked to the "mode" connector + digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and + loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority + + delay(5000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_withRemoteLoad_2.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, calibration values that are supplied in floating point + // form need to be rescaled. + // +// phaseCal_grid_int = phaseCal_grid * 256; // for integer maths <-- not supported +// phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths <-- not supported + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the correct scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples. The main processor and +// the ADC work autonomously, their operation being only linked via the dataReady flag. +// As soon as a new set of data is made available by the ADC, the main processor can +// start to work on it immediately. +// +void allGeneralProcessing() +{ + static boolean beyondStartUpPhase = false; // start-up delay, allows things to settle + static boolean triggerNeedsToBeArmed = false; // once per mains cycle (+ve half) + static int samplesDuringThisMainsCycle; // for normalising the power in each mains cycle + static long sumP_grid; // for per-cycle summation of 'real power' + static long sumP_diverted; // for per-cycle summation of 'real power' + static enum polarities polarityOfLastSampleV; // for zero-crossing detection + static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) + static long lastSampleVminusDC_long; // for the phaseCal algorithm + static byte perSecondCounter = 0; + + // remove DC offset from the raw voltage sample by subtracting the accurate value + // as determined by a LP filter. + long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long; + + // determine the polarity of the latest voltage sample + enum polarities polarityNow; + if(sampleVminusDC_long > 0) { + polarityNow = POSITIVE; } + else { + polarityNow = NEGATIVE; } + + if (polarityNow == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + cycleCount++; + + triggerNeedsToBeArmed = true; // the trigger is armed once during each +ve half-cycle + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / samplesDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / samplesDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- for non-standard use + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + if (EDD_isActive) // Energy Diversion Display + { + // When locally diverted energy is being monitored, the latest contribution + // needs to be added to an accumulator which operates with maximum precision. + // + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) { + realEnergy_diverted = 0; } // to avoid 'creep' + + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + perSecondCounter++; + if(perSecondCounter >= CYCLES_PER_SECOND) + { + perSecondCounter = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); // occurs every second + + Serial.println (energyInBucket_long * IEUtoJoulesConversion_CT1); + } + + + // when using the single-threshold power diversion algorithm, a counter needs + // to be reset whenever the energy level in the accumulator crosses the mid-point + // + enum energyStates energyStateOnLastLoop = energyStateNow; + + if (energyInBucket_long > energyThreshold_long) { + energyStateNow = UPPER_HALF; } + else { + energyStateNow = LOWER_HALF; } + + if (energyStateNow != energyStateOnLastLoop) { + cycleCountAtLastMidPointCrossing = cycleCount; } + + + // clear the per-cycle accumulators for use in this new mains cycle. + samplesDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + if (triggerNeedsToBeArmed == true) + { + // check to see whether the trigger device can now be reliably armed + if(samplesDuringThisMainsCycle == 3) // should always exceed 20V (the min for trigger) + { + boolean OKtoSendRFcommandNow = false; + boolean changeOfLoadState = false; + + // a pending RF command takes priority over the normal logic + if (sendRFcommandNextTime) + { + OKtoSendRFcommandNow = true; + sendRFcommandNextTime = false; + } + else + { + /* Now it's time to determine whether any of the the loads need to be changed. + * This is a 2-stage process: + * First, change the LOGICAL loads as necessary, then update the PHYSICAL + * loads according to the mapping that exists between them. The mapping is + * 1:1 by default but can be altered by a hardware switch which allows the + * priority of the remote load to be altered. + * This code uses a single-threshold algorithm which relies on regular switching + * of the load. + */ + if (cycleCount > cycleCountAtLastMidPointCrossing + postMidPointCrossingDelay_cycles) + { + if (energyInBucket_long > energyThreshold_long) + { + increaseLoadIfPossible(); // to reduce the level in the bucket + } + else + { + decreaseLoadIfPossible(); // to increase the level in the bucket + } + } + + + /* Update the state of all physical loads and determine whether the + * state of the remote load has changed. The remote load is hard-coded + * as Physical Load 1 (Physical Load 0 is a local load) + */ + boolean remoteLoadHasChangedState = false; + byte prevStateOfRemoteLoad = (byte)physicalLoadState[1]; + updatePhysicalLoadStates(); + if ((byte)physicalLoadState[1] != prevStateOfRemoteLoad) + { + remoteLoadHasChangedState = true; + } + + /* Now determine whether an RF command should be sent. This can be for + * either of two reasons: + * + * - the on/off state of the remote load needs to be changed + * - a refresh command is due ('cos no change of state has occurred recently) + * + * If the on/off state needs to be changed, but a refresh command was sent on the + * previous cycle, the 'change of state' command is deferred until the next + * cycle. A refresh command can always be sent straight away because it can + * be guaranteed that no command has been sent immediately beforehand. + */ + + // determine how long it's been since the last RF command was sent + long cycleCountSinceLastRFtransmission = cycleCount - cycleCountAtLastRFtransmission; + + if (remoteLoadHasChangedState) + { + // ensure that RF commands are not sent on consecutive cycles + if (cycleCountSinceLastRFtransmission > 1) + { + // the "change of state" can be acted on immediately + OKtoSendRFcommandNow = true; // local flag + } + else + { + // the "change of state" must be deferred until next cycle + sendRFcommandNextTime = true; // global flag + } + } + else + { + // no "change of state", so check whether a refresh command is due + if (cycleCountSinceLastRFtransmission >= noOfCyclesBeforeRefresh) + { + // a refresh command is due (which can always be sent immediately) + OKtoSendRFcommandNow = true; + } + } + } + + // update the local load, which is physical load 0 + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); + + // update the remote load, which is physical load 1 + if (OKtoSendRFcommandNow) + { + cycleCountAtLastRFtransmission = cycleCount; + tx_data.msgNumber = messageNumber++; + tx_data.dumpState = physicalLoadState[1]; +// tx_data.dumpState = messageNumber & 1; // for test only + send_rf_data(); + /* + Serial.print(tx_data.dumpState); + Serial.print(", "); + Serial.print(tx_data.msgNumber); + Serial.print("; "); + Serial.println(activeLoadID); // useful for keeping track of different priority loads + */ + } + + // update the Energy Diversion Detector + if (physicalLoadState[0] == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + + // clear the flag which ensures that loads are only updated once per mains cycle + triggerNeedsToBeArmed = false; + } + } + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > startUpPeriod * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + samplesDuringThisMainsCycle = 0; + Serial.println ("Go!"); + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + +// checkOutputModeSelection(); // updates outputMode if switch is changed + checkLoadPrioritySelection(); // updates load priorities if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is positive + + // processing for EVERY pair of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform +// long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform +// long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + samplesDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityOfLastSampleV = polarityNow; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + + +void increaseLoadIfPossible() +{ + /* if permitted by A/F rules, turn on the highest priority logical load that is not already on. + */ + boolean changed = false; + + // Only one load may operate freely at a time. Other loads are prevented from + // switching until a sufficient period has elapsed since the last transition, and + // then only if the energy level has not have fallen since the previous ON transition. + // These measures allow a lower priority load to contribute if a higher priority + // load is not having the desired effect, but not immediately. + // + if (energyInBucket_long >= energyAtLastOnTransition_long) + { + boolean timeout = (cycleCount > cycleCountAtLastTransition + interLoadSeparationDelay_cycles); + for (int i = 0; i < noOfDumploads && !changed; i++) + { + if (logicalLoadState[i] == LOAD_OFF) + { + if ((i == activeLoadID) || timeout) + { + logicalLoadState[i] = LOAD_ON; + cycleCountAtLastTransition = cycleCount; + energyAtLastOnTransition_long = energyInBucket_long; +// energyAtLastOffTransition_long = 3600; // reset the 'opposite' mechanism. <-WRONG! + energyAtLastOffTransition_long = capacityOfEnergyBucket_long; // reset the 'opposite' mechanism. + activeLoadID = i; + changed = true; + } + } + } + } + else + { + // energy level has not risen so there's no need to apply any more load + } +// return (changed); +} + +void decreaseLoadIfPossible() +{ + /* if permitted by A/F rules, turn off the highest priority logical load that is not already off. + */ + boolean changed = false; + + // Only one load may operate freely at a time. Other loads are prevented from + // switching until a sufficient period has elapsed since the last transition, and + // then only if the energy level has not have risen since the previous OFF transition. + // These measures allow a lower priority load to contribute if a higher priority + // load is not having the desired effect, but not immediately. + // + if (energyInBucket_long <= energyAtLastOnTransition_long) + { + boolean timeout = (cycleCount > cycleCountAtLastTransition + interLoadSeparationDelay_cycles); +// for (int i = 0; i < noOfDumploads && !done; i++) + for (int i = (noOfDumploads -1); i >= 0 && !changed; i--) + { + if (logicalLoadState[i] == LOAD_ON) + { + if ((i == activeLoadID) || timeout) + { + logicalLoadState[i] = LOAD_OFF; + cycleCountAtLastTransition = cycleCount; + energyAtLastOnTransition_long = energyInBucket_long; + energyAtLastOffTransition_long = 0; // reset the 'opposite' mechanism. + activeLoadID = i; + changed = true; + } + } + } + } + else + { + // energy level has not fallen so there's no need to apply any more load + } +// return (changed); +} + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * By default, the association between the physical and logical loads is 1:1. If + * the remote load is set to have priority, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * - Physical load 0 is local, and is controlled by use of an output pin. + * - Physical load 1 is remote, and is controlled via the associated RFM12B module. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } + + if (loadPriorityMode == REMOTE_HAS_PRIORITY) + { + // swap physical loads 0 & 1 if remote load has priority + physicalLoadState[0] = logicalLoadState[1]; + physicalLoadState[1] = logicalLoadState[0]; + } +} + + +// this function changes the value of the load priorities if the state of the external switch is altered +void checkLoadPrioritySelection() +{ + static byte loadPrioritySwitchCcount = 0; + int pinState = digitalRead(loadPrioritySelectorPin); + if (pinState != loadPriorityMode) + { + loadPrioritySwitchCcount++; + } + if (loadPrioritySwitchCcount >= 20) + { + loadPrioritySwitchCcount = 0; + loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable + Serial.print ("loadPriority selection changed to "); + if (loadPriorityMode == LOCAL_HAS_PRIORITY) { + Serial.println ( "local"); } + else { + Serial.println ( "remote"); } + } +} + + +// Although this sketch always operates in ANTI_FLICKER mode, it was convenient +// to leave this mechanism in place. +// +void configureParamsForSelectedOutputMode() +{ + energyThreshold_long = capacityOfEnergyBucket_long * 0.5; + + if (outputMode == ANTI_FLICKER) + { + postMidPointCrossingDelay_cycles = postMidPointCrossingDelayForAF_cycles; + } + else + { + postMidPointCrossingDelay_cycles = 0; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" energyThreshold_long = "); + Serial.println(energyThreshold_long); + Serial.print(" postMidPointCrossingDelay_cycles = "); + Serial.println(postMidPointCrossingDelay_cycles); + Serial.print(" interLoadSeparationDelay_cycles = "); + Serial.println(interLoadSeparationDelay_cycles); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +// valueToBeDisplayed++; +} + + + + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + +void send_rf_data() +{ + // rf12_sleep(RF12_WAKEUP); + // if ready to send + exit route if it gets stuck + int i = 0; + while (!rf12_canSend() && i<10) + { + rf12_recvDone(); + i++; + } + rf12_sendNow(0, &tx_data, sizeof tx_data); + + // rf12_sendStart(0, &tx_data, sizeof tx_data); + // rf12_sendWait(2); + // rf12_sleep(RF12_SLEEP); +} + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_withRemoteLoad_3.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_withRemoteLoad_3.ino new file mode 100644 index 0000000..3a271c5 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_withRemoteLoad_3.ino @@ -0,0 +1,1274 @@ +/* Mk2_RFremoteLoad_3.ino + * + * This sketch is for diverting surplus PV power to a local dump load using a triac. + * A remote load is also supported, this being controlled using the on-board + * RFM12B module. A switch is also supported which allows either load to be + * selected as having thei higher priority. If a local load is not provided, + * all surplus power is available for use by the remote load. + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The selector switch, as mentioned above, connects to the "mode" port. For this + * version of the Mk2 code, the system uses an alternative version of the anti-flicker + * algorithm which is well suited for multiple loads. As the 'normal' mode is no longer + * required, the 'mode' port can be re-assigned for priority selection. + * + * The integral voltage sensor is fed from one of the secondary coils of the transformer. + * Current is measured via Current Transformers at the CT1 and CT2 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the 'diverted' current, so that energy which is diverted via the + * local dump load can be recorded and displayed locally. + * + * A persistence-based 4-digit display is supported. When the RFM12B module is + * in use, the display can only be used in conjunction with an extra pair of + * logic chips. These are ICs 3 and 4, which reduce the number of processor pins + * that are needed to drive the display. + * + * This sketch has many similarities with Revision 5c of the Mk2i PV Router code that I + * have posted on the OpenEnergyMonitor forum. That version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * September 2014: renamed as Mk2_withRemoteLoad_3, with these changes: + * - reimplementation of cycleCount, as it could have overflowed with unpredictable results; + * - the functions increaseLoadIfPossible() and decreaseLoadIfPossible() have been tidied; + * - energyThreshold_long has been renamed as midPointOfEnergyBucket_long. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + * September 2014 + */ + +#include +#include // JeeLib is available at from: http://github.com/jcw/jeelib +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define SWEETZONE_IN_JOULES 3600 +#define REFRESH_PERIOD_IN_CYCLES 50 // max allowed interval between RF messages +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +const byte noOfDumploads = 2; // The logic expects a minimum of 2 dumploads, + // for local & remote loads, but neither has to + // be physically present. + +// definitions of enumerated types and instances of same +enum polarities {NEGATIVE, POSITIVE}; +enum outputModes {ANTI_FLICKER, NORMAL}; +enum loadPriorityModes {REMOTE_HAS_PRIORITY, LOCAL_HAS_PRIORITY}; + +enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are active low +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +enum energyStates {LOWER_HALF, UPPER_HALF}; // for single threshold AF algorithm +enum energyStates energyStateNow; + +// ---------------- Extra Features selection ---------------------- +// +// - WORKLOAD_CHECK, for determining how much spare processing time there is. +// +// #define WORKLOAD_CHECK // <-- Include this line is this feature is required + + +// For most single-load Mk2 systems, the power-diversion logic can operate in either of two modes: +// +// - NORMAL, where the triac switches rapidly on/off to maintain a constant energy level. +// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations +// of the local mains voltage. +// +// For this multi-load version, the same mechanism has been retained but the +// output mode is hard-coded as below: +enum outputModes outputMode = ANTI_FLICKER; + +// In this multi-load version, the external switch is re-used to determine the load priority +enum loadPriorityModes loadPriorityMode = LOCAL_HAS_PRIORITY; + +/* frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ + */ +#define freq RF12_868MHZ // Use the freq to match the module you have. + +const int nodeID = 10; // RFM12B node ID +const int networkGroup = 210; // RFM12B wireless network group - needs to be same as emonBase and emonGLCD +const int UNO = 1; // Set to 0 if you're not using the UNO bootloader (i.e using Duemilanove) + // - All Atmega's shipped from OpenEnergyMonitor come with Arduino Uno bootloader + +const int noOfCyclesBeforeRefresh = 50; // nominally every second, but not critical +//long cycleCountAtLastRFtransmission = 0; +int messageNumber = 0; + +// data structure for RF comms +typedef struct { byte dumpState; int msgNumber; } Tx_struct; // data for RF comms +Tx_struct tx_data; + + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +// D2 is for the RFM12B +const byte loadPrioritySelectorPin = 3; // <-- this is the "mode" port +const byte physicalLoad_0_pin = 4; // <-- this is the "trigger" port +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +// D10 is for the RFM12B +// D11 is for the RFM12B +// D12 is for the RFM12B +// D13 is for the RFM12B + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + + +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long midPointOfEnergyBucket_long; // used for 'normal' and single-threshold 'AF' logic +// int phaseCal_grid_int; // to avoid the need for floating-point maths +// int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; + +int postMidPointCrossingDelay_cycles; // assigned in setup(), different for each output mode +const int postMidPointCrossingDelayForAF_cycles = 25; // in 20 ms counts +const int interLoadSeparationDelay_cycles = 25; // in 20 ms cycle counts (for both output modes) +byte activeLoadID; // only one load may operate freely at a time. + +long energyAtLastOffTransition_long; +long energyAtLastOnTransition_long; +int mainsCyclesSinceLastMidPointCrossing = 0; +int mainsCyclesSinceLastChangeOfLoadState = 0; +int mainsCyclesSinceLastRF_tx = 0; + + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +int sampleI_grid; +int sampleI_diverted; +int sampleV; + +// for the remote load that is controlled by RF +unsigned long cycleCountAtLastTransmission = 0; +boolean sendRFcommandNextTime = false; + + + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// With most hardware, the default values are likely to work fine without +// need for change. A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When +// measuring a resistive load, the voltage and current waveforms should be perfectly +// aligned. In this situation, the Power Factor will be 1. +// +// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct +// value of phaseCal for any hardware configuration. An index of my various Mk2-related +// exhibits is available at http://openenergymonitor.org/emon/node/1757 +// +//const float phaseCal_grid = 1.0; <--- not used in this version +//const float phaseCal_diverted = 1.0; <--- not used in this version + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 10 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // Energy Diversion Detection (for the local load only) +long requiredExportPerMainsCycle_inIEU; +float IEUtoJoulesConversion_CT1; + + +void setup() +{ + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for the local dump-load + for(int i = 0; i< noOfDumploads; i++) + { + logicalLoadState[i] = LOAD_OFF; + physicalLoadState[i] = LOAD_OFF; + } + + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // force the local load to be in the "off" state. + + pinMode(loadPrioritySelectorPin, INPUT); // this pin is tracked to the "mode" connector + digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and + loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority + + delay(5000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_withRemoteLoad_3.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, calibration values that are supplied in floating point + // form need to be rescaled. + // +// phaseCal_grid_int = phaseCal_grid * 256; // for integer maths <-- not supported +// phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths <-- not supported + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up + midPointOfEnergyBucket_long = capacityOfEnergyBucket_long / 2; + energyAtLastOffTransition_long = midPointOfEnergyBucket_long; + energyAtLastOnTransition_long = midPointOfEnergyBucket_long; + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the correct scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples. The main processor and +// the ADC work autonomously, their operation being only linked via the dataReady flag. +// As soon as a new set of data is made available by the ADC, the main processor can +// start to work on it immediately. +// +void allGeneralProcessing() +{ + static boolean beyondStartUpPhase = false; // start-up delay, allows things to settle + static boolean triggerNeedsToBeArmed = false; // once per mains cycle (+ve half) + static int samplesDuringThisMainsCycle; // for normalising the power in each mains cycle + static long sumP_grid; // for per-cycle summation of 'real power' + static long sumP_diverted; // for per-cycle summation of 'real power' + static enum polarities polarityOfLastSampleV; // for zero-crossing detection + static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) + static long lastSampleVminusDC_long; // for the phaseCal algorithm + static byte perSecondCounter = 0; + + // remove DC offset from the raw voltage sample by subtracting the accurate value + // as determined by a LP filter. + long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long; + + // determine the polarity of the latest voltage sample + enum polarities polarityNow; + if(sampleVminusDC_long > 0) { + polarityNow = POSITIVE; } + else { + polarityNow = NEGATIVE; } + + if (polarityNow == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + // cycleCount++; <- this mechanism is unsafe, it will eventually overflow + mainsCyclesSinceLastMidPointCrossing++; + mainsCyclesSinceLastChangeOfLoadState++; + mainsCyclesSinceLastRF_tx++; + + triggerNeedsToBeArmed = true; // the trigger is armed once during each +ve half-cycle + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / samplesDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / samplesDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- for non-standard use + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + if (EDD_isActive) // Energy Diversion Display + { + // When locally diverted energy is being monitored, the latest contribution + // needs to be added to an accumulator which operates with maximum precision. + // + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) { + realEnergy_diverted = 0; } // to avoid 'creep' + + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + perSecondCounter++; + if(perSecondCounter >= CYCLES_PER_SECOND) + { + perSecondCounter = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); // occurs every second + + Serial.println (energyInBucket_long * IEUtoJoulesConversion_CT1); + } + + + // when using the single-threshold power diversion algorithm, a counter needs + // to be reset whenever the energy level in the accumulator crosses the mid-point + // + enum energyStates energyStateOnLastLoop = energyStateNow; + + if (energyInBucket_long > midPointOfEnergyBucket_long) { + energyStateNow = UPPER_HALF; } + else { + energyStateNow = LOWER_HALF; } + + if (energyStateNow != energyStateOnLastLoop) { + mainsCyclesSinceLastMidPointCrossing = 0; } + + + // clear the per-cycle accumulators for use in this new mains cycle. + samplesDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + if (triggerNeedsToBeArmed == true) + { + // check to see whether the trigger device can now be reliably armed + if(samplesDuringThisMainsCycle == 3) // should always exceed 20V (the min for trigger) + { + boolean OKtoSendRFcommandNow = false; + boolean changeOfLoadState = false; + + // a pending RF command takes priority over the normal logic + if (sendRFcommandNextTime) + { + OKtoSendRFcommandNow = true; + sendRFcommandNextTime = false; + } + else + { + /* Now it's time to determine whether any of the the loads need to be changed. + * This is a 2-stage process: + * First, change the LOGICAL loads as necessary, then update the PHYSICAL + * loads according to the mapping that exists between them. The mapping is + * 1:1 by default but can be altered by a hardware switch which allows the + * priority of the remote load to be altered. + * This code uses a single-threshold algorithm which relies on regular switching + * of the load. + */ + if (mainsCyclesSinceLastMidPointCrossing > postMidPointCrossingDelay_cycles) + { + if (energyInBucket_long > midPointOfEnergyBucket_long) + { + increaseLoadIfPossible(); // to reduce the level in the bucket + } + else + { + decreaseLoadIfPossible(); // to increase the level in the bucket + } + } + + + /* Update the state of all physical loads and determine whether the + * state of the remote load has changed. The remote load is hard-coded + * as Physical Load 1 (Physical Load 0 is a local load) + */ + boolean remoteLoadHasChangedState = false; + byte prevStateOfRemoteLoad = (byte)physicalLoadState[1]; + updatePhysicalLoadStates(); + if ((byte)physicalLoadState[1] != prevStateOfRemoteLoad) + { + remoteLoadHasChangedState = true; + } + + /* Now determine whether an RF command should be sent. This can be for + * either of two reasons: + * + * - the on/off state of the remote load needs to be changed + * - a refresh command is due ('cos no change of state has occurred recently) + * + * If the on/off state needs to be changed, but a refresh command was sent on the + * previous cycle, the 'change of state' command is deferred until the next + * cycle. A refresh command can always be sent straight away because it can + * be guaranteed that no command has been sent immediately beforehand. + */ + if (remoteLoadHasChangedState) + { + // ensure that RF commands are not sent on consecutive cycles + if (mainsCyclesSinceLastRF_tx > 1) + { + // the "change of state" can be acted on immediately + OKtoSendRFcommandNow = true; // local flag + } + else + { + // the "change of state" must be deferred until next cycle + sendRFcommandNextTime = true; // global flag + } + } + else + { + // no "change of state", so check whether a refresh command is due + if (mainsCyclesSinceLastRF_tx >= noOfCyclesBeforeRefresh) + { + // a refresh command is due (which can always be sent immediately) + OKtoSendRFcommandNow = true; + } + } + } + + // update the local load, which is physical load 0 + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); + + // update the remote load, which is physical load 1 + if (OKtoSendRFcommandNow) + { + mainsCyclesSinceLastRF_tx = 0; + tx_data.msgNumber = messageNumber++; + tx_data.dumpState = physicalLoadState[1]; +// tx_data.dumpState = messageNumber & 1; // for test only + send_rf_data(); +/* + Serial.print(tx_data.dumpState); + Serial.print(", "); + Serial.print(tx_data.msgNumber); + Serial.print("; "); + Serial.println(activeLoadID); // useful for keeping track of different priority loads +*/ + } + + // update the Energy Diversion Detector + if (physicalLoadState[0] == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + + // clear the flag which ensures that loads are only updated once per mains cycle + triggerNeedsToBeArmed = false; + } + } + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > startUpPeriod * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + samplesDuringThisMainsCycle = 0; + Serial.println ("Go!"); + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + +// checkOutputModeSelection(); // updates outputMode if switch is changed + checkLoadPrioritySelection(); // updates load priorities if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is positive + + // processing for EVERY pair of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform +// long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform +// long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + samplesDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityOfLastSampleV = polarityNow; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + + +void increaseLoadIfPossible() +{ + /* if permitted by A/F rules, turn on the highest priority logical load that is not already on. + */ + boolean changed = false; + + // Only one load may operate freely at a time. Other loads are prevented from + // switching until a sufficient period has elapsed since the last transition. + // This scheme allows a lower priority load to contribute if a higher priority + // load is not having the desired effect, but not immediately. + // + if (energyInBucket_long >= energyAtLastOnTransition_long) + { +// Serial.print('+'); // useful for testing this logic + boolean timeout = (mainsCyclesSinceLastChangeOfLoadState > interLoadSeparationDelay_cycles); + for (int i = 0; i < noOfDumploads && !changed; i++) + { + if (logicalLoadState[i] == LOAD_OFF) + { + if ((i == activeLoadID) || timeout) + { + logicalLoadState[i] = LOAD_ON; + mainsCyclesSinceLastChangeOfLoadState = 0; + energyAtLastOnTransition_long = energyInBucket_long; + energyAtLastOffTransition_long = midPointOfEnergyBucket_long; // reset the 'opposite' mechanism. + activeLoadID = i; + changed = true; + } + } + } + } + else + { + // energy level has not risen so there's no need to apply any more load + } +// return (changed); +} + +void decreaseLoadIfPossible() +{ + /* if permitted by A/F rules, turn off the lowest priority logical load that is not already off. + */ + boolean changed = false; + + // Only one load may operate freely at a time. Other loads are prevented from + // switching until a sufficient period has elapsed since the last transition. + // This scheme allows a lower priority load to contribute if a higher priority + // load is not having the desired effect, but not immediately. + // + if (energyInBucket_long <= energyAtLastOffTransition_long) + { +// Serial.print('-'); // useful for testing this logic + boolean timeout = (mainsCyclesSinceLastChangeOfLoadState > interLoadSeparationDelay_cycles); +// for (int i = 0; i < noOfDumploads && !done; i++) + for (int i = (noOfDumploads -1); i >= 0 && !changed; i--) + { + if (logicalLoadState[i] == LOAD_ON) + { + if ((i == activeLoadID) || timeout) + { + logicalLoadState[i] = LOAD_OFF; + mainsCyclesSinceLastChangeOfLoadState = 0; + energyAtLastOffTransition_long = energyInBucket_long; + energyAtLastOnTransition_long = midPointOfEnergyBucket_long; // reset the 'opposite' mechanism. + activeLoadID = i; + changed = true; + } + } + } + } + else + { + // energy level has not fallen so there's no need to remove any more load + } +// return (changed); +} + + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * By default, the association between the physical and logical loads is 1:1. If + * the remote load is set to have priority, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * - Physical load 0 is local, and is controlled by use of an output pin. + * - Physical load 1 is remote, and is controlled via the associated RFM12B module. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } + + if (loadPriorityMode == REMOTE_HAS_PRIORITY) + { + // swap physical loads 0 & 1 if remote load has priority + physicalLoadState[0] = logicalLoadState[1]; + physicalLoadState[1] = logicalLoadState[0]; + } +} + + +// this function changes the value of the load priorities if the state of the external switch is altered +void checkLoadPrioritySelection() +{ + static byte loadPrioritySwitchCcount = 0; + int pinState = digitalRead(loadPrioritySelectorPin); + if (pinState != loadPriorityMode) + { + loadPrioritySwitchCcount++; + } + if (loadPrioritySwitchCcount >= 20) + { + loadPrioritySwitchCcount = 0; + loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable + Serial.print ("loadPriority selection changed to "); + if (loadPriorityMode == LOCAL_HAS_PRIORITY) { + Serial.println ( "local"); } + else { + Serial.println ( "remote"); } + } +} + + +// Although this sketch always operates in ANTI_FLICKER mode, it was convenient +// to leave this mechanism in place. +// +void configureParamsForSelectedOutputMode() +{ + + if (outputMode == ANTI_FLICKER) + { + postMidPointCrossingDelay_cycles = postMidPointCrossingDelayForAF_cycles; + } + else + { + postMidPointCrossingDelay_cycles = 0; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" midPointOfEnergyBucket_long = "); + Serial.println(midPointOfEnergyBucket_long); + Serial.print(" postMidPointCrossingDelay_cycles = "); + Serial.println(postMidPointCrossingDelay_cycles); + Serial.print(" interLoadSeparationDelay_cycles = "); + Serial.println(interLoadSeparationDelay_cycles); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +// valueToBeDisplayed++; +} + + + + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + +void send_rf_data() +{ + // rf12_sleep(RF12_WAKEUP); + // if ready to send + exit route if it gets stuck + int i = 0; + while (!rf12_canSend() && i<10) + { + rf12_recvDone(); + i++; + } + rf12_sendNow(0, &tx_data, sizeof tx_data); + + // rf12_sendStart(0, &tx_data, sizeof tx_data); + // rf12_sendWait(2); + // rf12_sleep(RF12_SLEEP); +} + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_withRemoteLoad_4.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_withRemoteLoad_4.ino new file mode 100644 index 0000000..4fd1c3a --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_withRemoteLoad_4.ino @@ -0,0 +1,1331 @@ +/* Mk2_RFremoteLoad_4.ino + * + * This sketch is for diverting surplus PV power to a local dump load using a triac. + * A remote load is also supported, this being controlled using the on-board + * RFM12B module. An external switch allows either load to be selected as having + * thi higher priority. If a local load is not provided, all surplus power is + * available for use by the remote load. + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The selector switch, as mentioned above, connects to the "mode" port. For this + * version of the Mk2 code, the system uses an alternative version of the anti-flicker + * algorithm which is well suited for multiple loads. As the 'normal' mode is no longer + * required, the 'mode' port can be re-assigned for priority selection. + * + * The integral voltage sensor is fed from one of the secondary coils of the transformer. + * Current is measured via Current Transformers at the CT1 and CT2 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the 'diverted' current, so that energy which is diverted via the + * local dump load can be recorded and displayed locally. + * + * A persistence-based 4-digit display is supported. When the RFM12B module is + * in use, the display can only be used in conjunction with an extra pair of + * logic chips. These are ICs 3 and 4, which reduce the number of processor pins + * that are needed to drive the display. + * + * This sketch has many similarities with Revision 5c of the Mk2i PV Router code that I + * have posted on the OpenEnergyMonitor forum. That version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * September 2014: renamed as Mk2_withRemoteLoad_3, with these changes: + * - reimplementation of cycleCount, as it could have overflowed with unpredictable results; + * - the functions increaseLoadIfPossible() and decreaseLoadIfPossible() have been tidied; + * - energyThreshold_long has been renamed as midPointOfEnergyBucket_long. + * + * December 2014: renamed as Mk2_withRemoteLoad_4, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed); + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances; + * + * Robin Emley + * www.Mk2PVrouter.co.uk + * September 2014 + */ + +#include +#include // JeeLib is available at from: http://github.com/jcw/jeelib +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define SWEETZONE_IN_JOULES 3600 +#define REFRESH_PERIOD_IN_CYCLES 50 // max allowed interval between RF messages +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +const byte noOfDumploads = 2; // The logic expects a minimum of 2 dumploads, + // for local & remote loads, but neither has to + // be physically present. + +// definitions of enumerated types and instances of same +enum polarities {NEGATIVE, POSITIVE}; +enum outputModes {ANTI_FLICKER, NORMAL}; +enum loadPriorityModes {REMOTE_HAS_PRIORITY, LOCAL_HAS_PRIORITY}; + +enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are active low +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +enum energyStates {LOWER_HALF, UPPER_HALF}; // for single threshold AF algorithm +enum energyStates energyStateNow; + +// ---------------- Extra Features selection ---------------------- +// +// - WORKLOAD_CHECK, for determining how much spare processing time there is. +// +// #define WORKLOAD_CHECK // <-- Include this line is this feature is required + + +// For most single-load Mk2 systems, the power-diversion logic can operate in either of two modes: +// +// - NORMAL, where the triac switches rapidly on/off to maintain a constant energy level. +// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations +// of the local mains voltage. +// +// For this multi-load version, the same mechanism has been retained but the +// output mode is hard-coded as below: +enum outputModes outputMode = ANTI_FLICKER; + +// In this multi-load version, the external switch is re-used to determine the load priority +enum loadPriorityModes loadPriorityMode = LOCAL_HAS_PRIORITY; + +/* frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ + */ +#define freq RF12_868MHZ // Use the freq to match the module you have. + +const int nodeID = 10; // RFM12B node ID +const int networkGroup = 210; // RFM12B wireless network group - needs to be same as emonBase and emonGLCD +const int UNO = 1; // Set to 0 if you're not using the UNO bootloader (i.e using Duemilanove) + // - All Atmega's shipped from OpenEnergyMonitor come with Arduino Uno bootloader + +const int noOfCyclesBeforeRefresh = 50; // nominally every second, but not critical +//long cycleCountAtLastRFtransmission = 0; +int messageNumber = 0; + +// data structure for RF comms +typedef struct { byte dumpState; int msgNumber; } Tx_struct; // data for RF comms +Tx_struct tx_data; + + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +// D2 is for the RFM12B +const byte loadPrioritySelectorPin = 3; // <-- this is the "mode" port +const byte physicalLoad_0_pin = 4; // <-- this is the "trigger" port +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +// D10 is for the RFM12B +// D11 is for the RFM12B +// D12 is for the RFM12B +// D13 is for the RFM12B + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + + +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long midPointOfEnergyBucket_long; // used for 'normal' and single-threshold 'AF' logic +// int phaseCal_grid_int; // to avoid the need for floating-point maths +// int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; + +int postMidPointCrossingDelay_cycles; // assigned in setup(), different for each output mode +const int postMidPointCrossingDelayForAF_cycles = 25; // in 20 ms counts +const int interLoadSeparationDelay_cycles = 25; // in 20 ms cycle counts (for both output modes) +byte activeLoadID; // only one load may operate freely at a time. + +long energyAtLastOffTransition_long; +long energyAtLastOnTransition_long; +int mainsCyclesSinceLastMidPointCrossing = 0; +int mainsCyclesSinceLastChangeOfLoadState = 0; +int mainsCyclesSinceLastRF_tx = 0; + + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +int sampleI_grid; +int sampleI_diverted; +int sampleV; + +// for the remote load that is controlled by RF +unsigned long cycleCountAtLastTransmission = 0; +boolean sendRFcommandNextTime = false; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define POLARITY_CHECK_MAXCOUNT 3 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + + + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// With most hardware, the default values are likely to work fine without +// need for change. A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When +// measuring a resistive load, the voltage and current waveforms should be perfectly +// aligned. In this situation, the Power Factor will be 1. +// +// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct +// value of phaseCal for any hardware configuration. An index of my various Mk2-related +// exhibits is available at http://openenergymonitor.org/emon/node/1757 +// +//const float phaseCal_grid = 1.0; <--- not used in this version +//const float phaseCal_diverted = 1.0; <--- not used in this version + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 10 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // Energy Diversion Detection (for the local load only) +long requiredExportPerMainsCycle_inIEU; +float IEUtoJoulesConversion_CT1; + + +void setup() +{ + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for the local dump-load + for(int i = 0; i< noOfDumploads; i++) + { + logicalLoadState[i] = LOAD_OFF; + physicalLoadState[i] = LOAD_OFF; + } + + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // force the local load to be in the "off" state. + + pinMode(loadPrioritySelectorPin, INPUT); // this pin is tracked to the "mode" connector + digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and + loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority + + delay(5000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_withRemoteLoad_4.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, calibration values that are supplied in floating point + // form need to be rescaled. + // +// phaseCal_grid_int = phaseCal_grid * 256; // for integer maths <-- not supported +// phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths <-- not supported + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up + midPointOfEnergyBucket_long = capacityOfEnergyBucket_long / 2; + energyAtLastOffTransition_long = midPointOfEnergyBucket_long; + energyAtLastOnTransition_long = midPointOfEnergyBucket_long; + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the correct scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples. The main processor and +// the ADC work autonomously, their operation being only linked via the dataReady flag. +// As soon as a new set of data is made available by the ADC, the main processor can +// start to work on it immediately. +// +void allGeneralProcessing() +{ + static boolean beyondStartUpPhase = false; // start-up delay, allows things to settle + static boolean triggerNeedsToBeArmed = false; // once per mains cycle (+ve half) +// static int samplesDuringThisMainsCycle; // for normalising the power in each mains cycle + static long sumP_grid; // for per-cycle summation of 'real power' + static long sumP_diverted; // for per-cycle summation of 'real power' +// static enum polarities polarityOfLastSampleV; // for zero-crossing detection + static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) + static long lastSampleVminusDC_long; // for the phaseCal algorithm + static byte perSecondCounter = 0; + + // remove DC offset from the raw voltage sample by subtracting the accurate value + // as determined by a LP filter. + long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long; + + // determine the polarity of the latest voltage sample + enum polarities polarityNow; + if(sampleVminusDC_long > 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + // cycleCount++; <- this mechanism is unsafe, it will eventually overflow + mainsCyclesSinceLastMidPointCrossing++; + mainsCyclesSinceLastChangeOfLoadState++; + mainsCyclesSinceLastRF_tx++; + + // a simple routine for checking the continuity of the sampling scheme + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } + + triggerNeedsToBeArmed = true; // the trigger is armed once during each +ve half-cycle + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / sampleSetsDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- for non-standard use + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + if (EDD_isActive) // Energy Diversion Display + { + // When locally diverted energy is being monitored, the latest contribution + // needs to be added to an accumulator which operates with maximum precision. + // + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) { + realEnergy_diverted = 0; } // to avoid 'creep' + + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + perSecondCounter++; + if(perSecondCounter >= CYCLES_PER_SECOND) + { + perSecondCounter = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); // occurs every second + +// Serial.println (energyInBucket_long * IEUtoJoulesConversion_CT1); + } + + + // when using the single-threshold power diversion algorithm, a counter needs + // to be reset whenever the energy level in the accumulator crosses the mid-point + // + enum energyStates energyStateOnLastLoop = energyStateNow; + + if (energyInBucket_long > midPointOfEnergyBucket_long) { + energyStateNow = UPPER_HALF; } + else { + energyStateNow = LOWER_HALF; } + + if (energyStateNow != energyStateOnLastLoop) { + mainsCyclesSinceLastMidPointCrossing = 0; } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + if (triggerNeedsToBeArmed == true) + { + // check to see whether the trigger device can now be reliably armed + if(sampleSetsDuringThisMainsCycle == 3) // should always exceed 20V (the min for trigger) + { + boolean OKtoSendRFcommandNow = false; + boolean changeOfLoadState = false; + + // a pending RF command takes priority over the normal logic + if (sendRFcommandNextTime) + { + OKtoSendRFcommandNow = true; + sendRFcommandNextTime = false; + } + else + { + /* Now it's time to determine whether any of the the loads need to be changed. + * This is a 2-stage process: + * First, change the LOGICAL loads as necessary, then update the PHYSICAL + * loads according to the mapping that exists between them. The mapping is + * 1:1 by default but can be altered by a hardware switch which allows the + * priority of the remote load to be altered. + * This code uses a single-threshold algorithm which relies on regular switching + * of the load. + */ + if (mainsCyclesSinceLastMidPointCrossing > postMidPointCrossingDelay_cycles) + { + if (energyInBucket_long > midPointOfEnergyBucket_long) + { + increaseLoadIfPossible(); // to reduce the level in the bucket + } + else + { + decreaseLoadIfPossible(); // to increase the level in the bucket + } + } + + + /* Update the state of all physical loads and determine whether the + * state of the remote load has changed. The remote load is hard-coded + * as Physical Load 1 (Physical Load 0 is a local load) + */ + boolean remoteLoadHasChangedState = false; + byte prevStateOfRemoteLoad = (byte)physicalLoadState[1]; + updatePhysicalLoadStates(); + if ((byte)physicalLoadState[1] != prevStateOfRemoteLoad) + { + remoteLoadHasChangedState = true; + } + + /* Now determine whether an RF command should be sent. This can be for + * either of two reasons: + * + * - the on/off state of the remote load needs to be changed + * - a refresh command is due ('cos no change of state has occurred recently) + * + * If the on/off state needs to be changed, but a refresh command was sent on the + * previous cycle, the 'change of state' command is deferred until the next + * cycle. A refresh command can always be sent straight away because it can + * be guaranteed that no command has been sent immediately beforehand. + */ + if (remoteLoadHasChangedState) + { + // ensure that RF commands are not sent on consecutive cycles + if (mainsCyclesSinceLastRF_tx > 1) + { + // the "change of state" can be acted on immediately + OKtoSendRFcommandNow = true; // local flag + } + else + { + // the "change of state" must be deferred until next cycle + sendRFcommandNextTime = true; // global flag + } + } + else + { + // no "change of state", so check whether a refresh command is due + if (mainsCyclesSinceLastRF_tx >= noOfCyclesBeforeRefresh) + { + // a refresh command is due (which can always be sent immediately) + OKtoSendRFcommandNow = true; + } + } + } + + // update the local load, which is physical load 0 + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); + + // update the remote load, which is physical load 1 + if (OKtoSendRFcommandNow) + { + mainsCyclesSinceLastRF_tx = 0; + tx_data.msgNumber = messageNumber++; + tx_data.dumpState = physicalLoadState[1]; +// tx_data.dumpState = messageNumber & 1; // for test only + send_rf_data(); +/* + Serial.print(tx_data.dumpState); + Serial.print(", "); + Serial.print(tx_data.msgNumber); + Serial.print("; "); + Serial.println(activeLoadID); // useful for keeping track of different priority loads +*/ + } + + // update the Energy Diversion Detector + if (physicalLoadState[0] == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + + // clear the flag which ensures that loads are only updated once per mains cycle + triggerNeedsToBeArmed = false; + } + } + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > startUpPeriod * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringThisMainsCycle = 0; + sampleCount_forContinuityChecker = 0; + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + +// checkOutputModeSelection(); // updates outputMode if switch is changed + checkLoadPrioritySelection(); // updates load priorities if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is positive + + // processing for EVERY pair of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform +// long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform +// long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + sampleSetsDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count >= POLARITY_CHECK_MAXCOUNT) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + +void increaseLoadIfPossible() +{ + /* if permitted by A/F rules, turn on the highest priority logical load that is not already on. + */ + boolean changed = false; + + // Only one load may operate freely at a time. Other loads are prevented from + // switching until a sufficient period has elapsed since the last transition. + // This scheme allows a lower priority load to contribute if a higher priority + // load is not having the desired effect, but not immediately. + // + if (energyInBucket_long >= energyAtLastOnTransition_long) + { +// Serial.print('+'); // useful for testing this logic + boolean timeout = (mainsCyclesSinceLastChangeOfLoadState > interLoadSeparationDelay_cycles); + for (int i = 0; i < noOfDumploads && !changed; i++) + { + if (logicalLoadState[i] == LOAD_OFF) + { + if ((i == activeLoadID) || timeout) + { + logicalLoadState[i] = LOAD_ON; + mainsCyclesSinceLastChangeOfLoadState = 0; + energyAtLastOnTransition_long = energyInBucket_long; + energyAtLastOffTransition_long = midPointOfEnergyBucket_long; // reset the 'opposite' mechanism. + activeLoadID = i; + changed = true; + } + } + } + } + else + { + // energy level has not risen so there's no need to apply any more load + } +// return (changed); +} + +void decreaseLoadIfPossible() +{ + /* if permitted by A/F rules, turn off the lowest priority logical load that is not already off. + */ + boolean changed = false; + + // Only one load may operate freely at a time. Other loads are prevented from + // switching until a sufficient period has elapsed since the last transition. + // This scheme allows a lower priority load to contribute if a higher priority + // load is not having the desired effect, but not immediately. + // + if (energyInBucket_long <= energyAtLastOffTransition_long) + { +// Serial.print('-'); // useful for testing this logic + boolean timeout = (mainsCyclesSinceLastChangeOfLoadState > interLoadSeparationDelay_cycles); +// for (int i = 0; i < noOfDumploads && !done; i++) + for (int i = (noOfDumploads -1); i >= 0 && !changed; i--) + { + if (logicalLoadState[i] == LOAD_ON) + { + if ((i == activeLoadID) || timeout) + { + logicalLoadState[i] = LOAD_OFF; + mainsCyclesSinceLastChangeOfLoadState = 0; + energyAtLastOffTransition_long = energyInBucket_long; + energyAtLastOnTransition_long = midPointOfEnergyBucket_long; // reset the 'opposite' mechanism. + activeLoadID = i; + changed = true; + } + } + } + } + else + { + // energy level has not fallen so there's no need to remove any more load + } +// return (changed); +} + + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * By default, the association between the physical and logical loads is 1:1. If + * the remote load is set to have priority, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * - Physical load 0 is local, and is controlled by use of an output pin. + * - Physical load 1 is remote, and is controlled via the associated RFM12B module. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } + + if (loadPriorityMode == REMOTE_HAS_PRIORITY) + { + // swap physical loads 0 & 1 if remote load has priority + physicalLoadState[0] = logicalLoadState[1]; + physicalLoadState[1] = logicalLoadState[0]; + } +} + + +// this function changes the value of the load priorities if the state of the external switch is altered +void checkLoadPrioritySelection() +{ + static byte loadPrioritySwitchCcount = 0; + int pinState = digitalRead(loadPrioritySelectorPin); + if (pinState != loadPriorityMode) + { + loadPrioritySwitchCcount++; + } + if (loadPrioritySwitchCcount >= 20) + { + loadPrioritySwitchCcount = 0; + loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable + Serial.print ("loadPriority selection changed to "); + if (loadPriorityMode == LOCAL_HAS_PRIORITY) { + Serial.println ( "local"); } + else { + Serial.println ( "remote"); } + } +} + + +// Although this sketch always operates in ANTI_FLICKER mode, it was convenient +// to leave this mechanism in place. +// +void configureParamsForSelectedOutputMode() +{ + + if (outputMode == ANTI_FLICKER) + { + postMidPointCrossingDelay_cycles = postMidPointCrossingDelayForAF_cycles; + } + else + { + postMidPointCrossingDelay_cycles = 0; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" midPointOfEnergyBucket_long = "); + Serial.println(midPointOfEnergyBucket_long); + Serial.print(" postMidPointCrossingDelay_cycles = "); + Serial.println(postMidPointCrossingDelay_cycles); + Serial.print(" interLoadSeparationDelay_cycles = "); + Serial.println(interLoadSeparationDelay_cycles); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +// valueToBeDisplayed++; +} + + + + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + +void send_rf_data() +{ + // rf12_sleep(RF12_WAKEUP); + // if ready to send + exit route if it gets stuck + int i = 0; + while (!rf12_canSend() && i<10) + { + rf12_recvDone(); + i++; + } + rf12_sendNow(0, &tx_data, sizeof tx_data); + + // rf12_sendStart(0, &tx_data, sizeof tx_data); + // rf12_sendWait(2); + // rf12_sleep(RF12_SLEEP); +} + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_withRemoteLoad_4a.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_withRemoteLoad_4a.ino new file mode 100644 index 0000000..b03fa21 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_withRemoteLoad_4a.ino @@ -0,0 +1,1341 @@ +/* Mk2_RFremoteLoad_4a.ino + * + * This sketch is for diverting surplus PV power to a local dump load using a triac. + * A remote load is also supported, this being controlled using the on-board + * RFM12B module. An external switch allows either load to be selected as having + * thi higher priority. If a local load is not provided, all surplus power is + * available for use by the remote load. + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The selector switch, as mentioned above, connects to the "mode" port. For this + * version of the Mk2 code, the system uses an alternative version of the anti-flicker + * algorithm which is well suited for multiple loads. As the 'normal' mode is no longer + * required, the 'mode' port can be re-assigned for priority selection. + * + * The integral voltage sensor is fed from one of the secondary coils of the transformer. + * Current is measured via Current Transformers at the CT1 and CT2 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the 'diverted' current, so that energy which is diverted via the + * local dump load can be recorded and displayed locally. + * + * A persistence-based 4-digit display is supported. When the RFM12B module is + * in use, the display can only be used in conjunction with an extra pair of + * logic chips. These are ICs 3 and 4, which reduce the number of processor pins + * that are needed to drive the display. + * + * This sketch has many similarities with Revision 5c of the Mk2i PV Router code that I + * have posted on the OpenEnergyMonitor forum. That version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * September 2014: renamed as Mk2_withRemoteLoad_3, with these changes: + * - reimplementation of cycleCount, as it could have overflowed with unpredictable results; + * - the functions increaseLoadIfPossible() and decreaseLoadIfPossible() have been tidied; + * - energyThreshold_long has been renamed as midPointOfEnergyBucket_long. + * + * December 2014: renamed as Mk2_withRemoteLoad_4, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed); + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances; + * + * January 2016: renamed as Mk2_withRemoteLoad_4a, with these changes: + * - a minor change in the routine timerIsr() has removed a timing uncertainty. + * - support for the RF69 RF module has been added. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#define RF69_COMPAT 0 // <-- include this line for the RFM12B +// #define RF69_COMPAT 1 // <-- include this line for the RF69 + +#include +#include // JeeLib is available at from: http://github.com/jcw/jeelib +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define SWEETZONE_IN_JOULES 3600 +#define REFRESH_PERIOD_IN_CYCLES 50 // max allowed interval between RF messages +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +const byte noOfDumploads = 2; // The logic expects a minimum of 2 dumploads, + // for local & remote loads, but neither has to + // be physically present. + +// definitions of enumerated types and instances of same +enum polarities {NEGATIVE, POSITIVE}; +enum outputModes {ANTI_FLICKER, NORMAL}; +enum loadPriorityModes {REMOTE_HAS_PRIORITY, LOCAL_HAS_PRIORITY}; + +enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are active low +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +enum energyStates {LOWER_HALF, UPPER_HALF}; // for single threshold AF algorithm +enum energyStates energyStateNow; + +// ---------------- Extra Features selection ---------------------- +// +// - WORKLOAD_CHECK, for determining how much spare processing time there is. +// +// #define WORKLOAD_CHECK // <-- Include this line is this feature is required + + +// For most single-load Mk2 systems, the power-diversion logic can operate in either of two modes: +// +// - NORMAL, where the triac switches rapidly on/off to maintain a constant energy level. +// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations +// of the local mains voltage. +// +// For this multi-load version, the same mechanism has been retained but the +// output mode is hard-coded as below: +enum outputModes outputMode = ANTI_FLICKER; + +// In this multi-load version, the external switch is re-used to determine the load priority +enum loadPriorityModes loadPriorityMode = LOCAL_HAS_PRIORITY; + +/* frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ + */ +#define freq RF12_433MHZ // Use the freq to match the module you have. + +const int nodeID = 10; // RFM12B node ID +const int networkGroup = 210; // RFM12B wireless network group - needs to be same as emonBase and emonGLCD +const int UNO = 1; // Set to 0 if you're not using the UNO bootloader (i.e using Duemilanove) + // - All Atmega's shipped from OpenEnergyMonitor come with Arduino Uno bootloader + +const int noOfCyclesBeforeRefresh = 50; // nominally every second, but not critical +//long cycleCountAtLastRFtransmission = 0; +int messageNumber = 0; + +// data structure for RF comms +typedef struct { byte dumpState; int msgNumber; } Tx_struct; // data for RF comms +Tx_struct tx_data; + + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +// D2 is for the RFM12B +const byte loadPrioritySelectorPin = 3; // <-- this is the "mode" port +const byte physicalLoad_0_pin = 4; // <-- this is the "trigger" port +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +// D10 is for the RFM12B +// D11 is for the RFM12B +// D12 is for the RFM12B +// D13 is for the RFM12B + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + + +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long midPointOfEnergyBucket_long; // used for 'normal' and single-threshold 'AF' logic +// int phaseCal_grid_int; // to avoid the need for floating-point maths +// int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; + +int postMidPointCrossingDelay_cycles; // assigned in setup(), different for each output mode +const int postMidPointCrossingDelayForAF_cycles = 25; // in 20 ms counts +const int interLoadSeparationDelay_cycles = 25; // in 20 ms cycle counts (for both output modes) +byte activeLoadID; // only one load may operate freely at a time. + +long energyAtLastOffTransition_long; +long energyAtLastOnTransition_long; +int mainsCyclesSinceLastMidPointCrossing = 0; +int mainsCyclesSinceLastChangeOfLoadState = 0; +int mainsCyclesSinceLastRF_tx = 0; + + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +int sampleI_grid; +int sampleI_diverted; +int sampleV; + +// for the remote load that is controlled by RF +unsigned long cycleCountAtLastTransmission = 0; +boolean sendRFcommandNextTime = false; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define POLARITY_CHECK_MAXCOUNT 3 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + + + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// With most hardware, the default values are likely to work fine without +// need for change. A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When +// measuring a resistive load, the voltage and current waveforms should be perfectly +// aligned. In this situation, the Power Factor will be 1. +// +// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct +// value of phaseCal for any hardware configuration. An index of my various Mk2-related +// exhibits is available at http://openenergymonitor.org/emon/node/1757 +// +//const float phaseCal_grid = 1.0; <--- not used in this version +//const float phaseCal_diverted = 1.0; <--- not used in this version + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // Energy Diversion Detection (for the local load only) +long requiredExportPerMainsCycle_inIEU; +float IEUtoJoulesConversion_CT1; + + +void setup() +{ + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for the local dump-load + for(int i = 0; i< noOfDumploads; i++) + { + logicalLoadState[i] = LOAD_OFF; + physicalLoadState[i] = LOAD_OFF; + } + + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // force the local load to be in the "off" state. + + pinMode(loadPrioritySelectorPin, INPUT); // this pin is tracked to the "mode" connector + digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and + loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority + + delay(5000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_withRemoteLoad_4a.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, calibration values that are supplied in floating point + // form need to be rescaled. + // +// phaseCal_grid_int = phaseCal_grid * 256; // for integer maths <-- not supported +// phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths <-- not supported + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up + midPointOfEnergyBucket_long = capacityOfEnergyBucket_long / 2; + energyAtLastOffTransition_long = midPointOfEnergyBucket_long; + energyAtLastOnTransition_long = midPointOfEnergyBucket_long; + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the correct scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples. The main processor and +// the ADC work autonomously, their operation being only linked via the dataReady flag. +// As soon as a new set of data is made available by the ADC, the main processor can +// start to work on it immediately. +// +void allGeneralProcessing() +{ + static boolean beyondStartUpPhase = false; // start-up delay, allows things to settle + static boolean triggerNeedsToBeArmed = false; // once per mains cycle (+ve half) +// static int samplesDuringThisMainsCycle; // for normalising the power in each mains cycle + static long sumP_grid; // for per-cycle summation of 'real power' + static long sumP_diverted; // for per-cycle summation of 'real power' +// static enum polarities polarityOfLastSampleV; // for zero-crossing detection + static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) + static long lastSampleVminusDC_long; // for the phaseCal algorithm + static byte perSecondCounter = 0; + + // remove DC offset from the raw voltage sample by subtracting the accurate value + // as determined by a LP filter. + long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long; + + // determine the polarity of the latest voltage sample + enum polarities polarityNow; + if(sampleVminusDC_long > 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + // cycleCount++; <- this mechanism is unsafe, it will eventually overflow + mainsCyclesSinceLastMidPointCrossing++; + mainsCyclesSinceLastChangeOfLoadState++; + mainsCyclesSinceLastRF_tx++; + + // a simple routine for checking the continuity of the sampling scheme + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } + + triggerNeedsToBeArmed = true; // the trigger is armed once during each +ve half-cycle + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / sampleSetsDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- for non-standard use + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + if (EDD_isActive) // Energy Diversion Display + { + // When locally diverted energy is being monitored, the latest contribution + // needs to be added to an accumulator which operates with maximum precision. + // + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) { + realEnergy_diverted = 0; } // to avoid 'creep' + + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + perSecondCounter++; + if(perSecondCounter >= CYCLES_PER_SECOND) + { + perSecondCounter = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); // occurs every second + +// Serial.println (energyInBucket_long * IEUtoJoulesConversion_CT1); + } + + + // when using the single-threshold power diversion algorithm, a counter needs + // to be reset whenever the energy level in the accumulator crosses the mid-point + // + enum energyStates energyStateOnLastLoop = energyStateNow; + + if (energyInBucket_long > midPointOfEnergyBucket_long) { + energyStateNow = UPPER_HALF; } + else { + energyStateNow = LOWER_HALF; } + + if (energyStateNow != energyStateOnLastLoop) { + mainsCyclesSinceLastMidPointCrossing = 0; } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + if (triggerNeedsToBeArmed == true) + { + // check to see whether the trigger device can now be reliably armed + if(sampleSetsDuringThisMainsCycle == 3) // should always exceed 20V (the min for trigger) + { + boolean OKtoSendRFcommandNow = false; + boolean changeOfLoadState = false; + + // a pending RF command takes priority over the normal logic + if (sendRFcommandNextTime) + { + OKtoSendRFcommandNow = true; + sendRFcommandNextTime = false; + } + else + { + /* Now it's time to determine whether any of the the loads need to be changed. + * This is a 2-stage process: + * First, change the LOGICAL loads as necessary, then update the PHYSICAL + * loads according to the mapping that exists between them. The mapping is + * 1:1 by default but can be altered by a hardware switch which allows the + * priority of the remote load to be altered. + * This code uses a single-threshold algorithm which relies on regular switching + * of the load. + */ + if (mainsCyclesSinceLastMidPointCrossing > postMidPointCrossingDelay_cycles) + { + if (energyInBucket_long > midPointOfEnergyBucket_long) + { + increaseLoadIfPossible(); // to reduce the level in the bucket + } + else + { + decreaseLoadIfPossible(); // to increase the level in the bucket + } + } + + + /* Update the state of all physical loads and determine whether the + * state of the remote load has changed. The remote load is hard-coded + * as Physical Load 1 (Physical Load 0 is a local load) + */ + boolean remoteLoadHasChangedState = false; + byte prevStateOfRemoteLoad = (byte)physicalLoadState[1]; + updatePhysicalLoadStates(); + if ((byte)physicalLoadState[1] != prevStateOfRemoteLoad) + { + remoteLoadHasChangedState = true; + } + + /* Now determine whether an RF command should be sent. This can be for + * either of two reasons: + * + * - the on/off state of the remote load needs to be changed + * - a refresh command is due ('cos no change of state has occurred recently) + * + * If the on/off state needs to be changed, but a refresh command was sent on the + * previous cycle, the 'change of state' command is deferred until the next + * cycle. A refresh command can always be sent straight away because it can + * be guaranteed that no command has been sent immediately beforehand. + */ + if (remoteLoadHasChangedState) + { + // ensure that RF commands are not sent on consecutive cycles + if (mainsCyclesSinceLastRF_tx > 1) + { + // the "change of state" can be acted on immediately + OKtoSendRFcommandNow = true; // local flag + } + else + { + // the "change of state" must be deferred until next cycle + sendRFcommandNextTime = true; // global flag + } + } + else + { + // no "change of state", so check whether a refresh command is due + if (mainsCyclesSinceLastRF_tx >= noOfCyclesBeforeRefresh) + { + // a refresh command is due (which can always be sent immediately) + OKtoSendRFcommandNow = true; + } + } + } + + // update the local load, which is physical load 0 + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); + + // update the remote load, which is physical load 1 + if (OKtoSendRFcommandNow) + { + mainsCyclesSinceLastRF_tx = 0; + tx_data.msgNumber = messageNumber++; + tx_data.dumpState = physicalLoadState[1]; +// tx_data.dumpState = messageNumber & 1; // for test only + send_rf_data(); +/* + Serial.print(tx_data.dumpState); + Serial.print(", "); + Serial.print(tx_data.msgNumber); + Serial.print("; "); + Serial.println(activeLoadID); // useful for keeping track of different priority loads +*/ + } + + // update the Energy Diversion Detector + if (physicalLoadState[0] == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + + // clear the flag which ensures that loads are only updated once per mains cycle + triggerNeedsToBeArmed = false; + } + } + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > startUpPeriod * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringThisMainsCycle = 0; + sampleCount_forContinuityChecker = 0; + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + +// checkOutputModeSelection(); // updates outputMode if switch is changed + checkLoadPrioritySelection(); // updates load priorities if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is positive + + // processing for EVERY pair of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform +// long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform +// long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + sampleSetsDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count >= POLARITY_CHECK_MAXCOUNT) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + +void increaseLoadIfPossible() +{ + /* if permitted by A/F rules, turn on the highest priority logical load that is not already on. + */ + boolean changed = false; + + // Only one load may operate freely at a time. Other loads are prevented from + // switching until a sufficient period has elapsed since the last transition. + // This scheme allows a lower priority load to contribute if a higher priority + // load is not having the desired effect, but not immediately. + // + if (energyInBucket_long >= energyAtLastOnTransition_long) + { +// Serial.print('+'); // useful for testing this logic + boolean timeout = (mainsCyclesSinceLastChangeOfLoadState > interLoadSeparationDelay_cycles); + for (int i = 0; i < noOfDumploads && !changed; i++) + { + if (logicalLoadState[i] == LOAD_OFF) + { + if ((i == activeLoadID) || timeout) + { + logicalLoadState[i] = LOAD_ON; + mainsCyclesSinceLastChangeOfLoadState = 0; + energyAtLastOnTransition_long = energyInBucket_long; + energyAtLastOffTransition_long = midPointOfEnergyBucket_long; // reset the 'opposite' mechanism. + activeLoadID = i; + changed = true; + } + } + } + } + else + { + // energy level has not risen so there's no need to apply any more load + } +// return (changed); +} + +void decreaseLoadIfPossible() +{ + /* if permitted by A/F rules, turn off the lowest priority logical load that is not already off. + */ + boolean changed = false; + + // Only one load may operate freely at a time. Other loads are prevented from + // switching until a sufficient period has elapsed since the last transition. + // This scheme allows a lower priority load to contribute if a higher priority + // load is not having the desired effect, but not immediately. + // + if (energyInBucket_long <= energyAtLastOffTransition_long) + { +// Serial.print('-'); // useful for testing this logic + boolean timeout = (mainsCyclesSinceLastChangeOfLoadState > interLoadSeparationDelay_cycles); +// for (int i = 0; i < noOfDumploads && !done; i++) + for (int i = (noOfDumploads -1); i >= 0 && !changed; i--) + { + if (logicalLoadState[i] == LOAD_ON) + { + if ((i == activeLoadID) || timeout) + { + logicalLoadState[i] = LOAD_OFF; + mainsCyclesSinceLastChangeOfLoadState = 0; + energyAtLastOffTransition_long = energyInBucket_long; + energyAtLastOnTransition_long = midPointOfEnergyBucket_long; // reset the 'opposite' mechanism. + activeLoadID = i; + changed = true; + } + } + } + } + else + { + // energy level has not fallen so there's no need to remove any more load + } +// return (changed); +} + + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * By default, the association between the physical and logical loads is 1:1. If + * the remote load is set to have priority, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * - Physical load 0 is local, and is controlled by use of an output pin. + * - Physical load 1 is remote, and is controlled via the associated RFM12B module. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } + + if (loadPriorityMode == REMOTE_HAS_PRIORITY) + { + // swap physical loads 0 & 1 if remote load has priority + physicalLoadState[0] = logicalLoadState[1]; + physicalLoadState[1] = logicalLoadState[0]; + } +} + + +// this function changes the value of the load priorities if the state of the external switch is altered +void checkLoadPrioritySelection() +{ + static byte loadPrioritySwitchCcount = 0; + int pinState = digitalRead(loadPrioritySelectorPin); + if (pinState != loadPriorityMode) + { + loadPrioritySwitchCcount++; + } + if (loadPrioritySwitchCcount >= 20) + { + loadPrioritySwitchCcount = 0; + loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable + Serial.print ("loadPriority selection changed to "); + if (loadPriorityMode == LOCAL_HAS_PRIORITY) { + Serial.println ( "local"); } + else { + Serial.println ( "remote"); } + } +} + + +// Although this sketch always operates in ANTI_FLICKER mode, it was convenient +// to leave this mechanism in place. +// +void configureParamsForSelectedOutputMode() +{ + + if (outputMode == ANTI_FLICKER) + { + postMidPointCrossingDelay_cycles = postMidPointCrossingDelayForAF_cycles; + } + else + { + postMidPointCrossingDelay_cycles = 0; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" midPointOfEnergyBucket_long = "); + Serial.println(midPointOfEnergyBucket_long); + Serial.print(" postMidPointCrossingDelay_cycles = "); + Serial.println(postMidPointCrossingDelay_cycles); + Serial.print(" interLoadSeparationDelay_cycles = "); + Serial.println(interLoadSeparationDelay_cycles); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +// valueToBeDisplayed++; +} + + + + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + +void send_rf_data() +{ + // rf12_sleep(RF12_WAKEUP); + // if ready to send + exit route if it gets stuck + int i = 0; + while (!rf12_canSend() && i<10) + { + rf12_recvDone(); + i++; + } + rf12_sendNow(0, &tx_data, sizeof tx_data); + + // rf12_sendStart(0, &tx_data, sizeof tx_data); + // rf12_sendWait(2); + // rf12_sleep(RF12_SLEEP); +} + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_withRemoteLoad_4b.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_withRemoteLoad_4b.ino new file mode 100644 index 0000000..3201e3c --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_withRemoteLoad_4b.ino @@ -0,0 +1,1345 @@ +/* Mk2_RFremoteLoad_4b.ino + * + * This sketch is for diverting surplus PV power to a local dump load using a triac. + * A remote load is also supported, this being controlled using the on-board + * RFM12B module. An external switch allows either load to be selected as having + * thi higher priority. If a local load is not provided, all surplus power is + * available for use by the remote load. + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The selector switch, as mentioned above, connects to the "mode" port. For this + * version of the Mk2 code, the system uses an alternative version of the anti-flicker + * algorithm which is well suited for multiple loads. As the 'normal' mode is no longer + * required, the 'mode' port can be re-assigned for priority selection. + * + * The integral voltage sensor is fed from one of the secondary coils of the transformer. + * Current is measured via Current Transformers at the CT1 and CT2 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the 'diverted' current, so that energy which is diverted via the + * local dump load can be recorded and displayed locally. + * + * A persistence-based 4-digit display is supported. When the RFM12B module is + * in use, the display can only be used in conjunction with an extra pair of + * logic chips. These are ICs 3 and 4, which reduce the number of processor pins + * that are needed to drive the display. + * + * This sketch has many similarities with Revision 5c of the Mk2i PV Router code that I + * have posted on the OpenEnergyMonitor forum. That version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * September 2014: renamed as Mk2_withRemoteLoad_3, with these changes: + * - reimplementation of cycleCount, as it could have overflowed with unpredictable results; + * - the functions increaseLoadIfPossible() and decreaseLoadIfPossible() have been tidied; + * - energyThreshold_long has been renamed as midPointOfEnergyBucket_long. + * + * December 2014: renamed as Mk2_withRemoteLoad_4, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed); + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances; + * + * January 2016: renamed as Mk2_withRemoteLoad_4a, with these changes: + * - a minor change in the routine timerIsr() has removed a timing uncertainty. + * - support for the RF69 RF module has been added. + * + * January 2016: updated to Mk2_withRemoteLoad_4b: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#define RF69_COMPAT 0 // <-- include this line for the RFM12B +// #define RF69_COMPAT 1 // <-- include this line for the RF69 + +#include +#include // JeeLib is available at from: http://github.com/jcw/jeelib +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define SWEETZONE_IN_JOULES 3600 +#define REFRESH_PERIOD_IN_CYCLES 50 // max allowed interval between RF messages +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +const byte noOfDumploads = 2; // The logic expects a minimum of 2 dumploads, + // for local & remote loads, but neither has to + // be physically present. + +// definitions of enumerated types and instances of same +enum polarities {NEGATIVE, POSITIVE}; +enum outputModes {ANTI_FLICKER, NORMAL}; +enum loadPriorityModes {REMOTE_HAS_PRIORITY, LOCAL_HAS_PRIORITY}; + +enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are active low +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +enum energyStates {LOWER_HALF, UPPER_HALF}; // for single threshold AF algorithm +enum energyStates energyStateNow; + +// ---------------- Extra Features selection ---------------------- +// +// - WORKLOAD_CHECK, for determining how much spare processing time there is. +// +// #define WORKLOAD_CHECK // <-- Include this line is this feature is required + + +// For most single-load Mk2 systems, the power-diversion logic can operate in either of two modes: +// +// - NORMAL, where the triac switches rapidly on/off to maintain a constant energy level. +// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations +// of the local mains voltage. +// +// For this multi-load version, the same mechanism has been retained but the +// output mode is hard-coded as below: +enum outputModes outputMode = ANTI_FLICKER; + +// In this multi-load version, the external switch is re-used to determine the load priority +enum loadPriorityModes loadPriorityMode = LOCAL_HAS_PRIORITY; + +/* frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ + */ +#define freq RF12_433MHZ // Use the freq to match the module you have. + +const int nodeID = 10; // RFM12B node ID +const int networkGroup = 210; // RFM12B wireless network group - needs to be same as emonBase and emonGLCD +const int UNO = 1; // Set to 0 if you're not using the UNO bootloader (i.e using Duemilanove) + // - All Atmega's shipped from OpenEnergyMonitor come with Arduino Uno bootloader + +const int noOfCyclesBeforeRefresh = 50; // nominally every second, but not critical +//long cycleCountAtLastRFtransmission = 0; +int messageNumber = 0; + +// data structure for RF comms +typedef struct { byte dumpState; int msgNumber; } Tx_struct; // data for RF comms +Tx_struct tx_data; + + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +// D2 is for the RFM12B +const byte loadPrioritySelectorPin = 3; // <-- this is the "mode" port +const byte physicalLoad_0_pin = 4; // <-- this is the "trigger" port +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +// D10 is for the RFM12B +// D11 is for the RFM12B +// D12 is for the RFM12B +// D13 is for the RFM12B + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + + +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long midPointOfEnergyBucket_long; // used for 'normal' and single-threshold 'AF' logic +// int phaseCal_grid_int; // to avoid the need for floating-point maths +// int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; + +int postMidPointCrossingDelay_cycles; // assigned in setup(), different for each output mode +const int postMidPointCrossingDelayForAF_cycles = 25; // in 20 ms counts +const int interLoadSeparationDelay_cycles = 25; // in 20 ms cycle counts (for both output modes) +byte activeLoadID; // only one load may operate freely at a time. + +long energyAtLastOffTransition_long; +long energyAtLastOnTransition_long; +int mainsCyclesSinceLastMidPointCrossing = 0; +int mainsCyclesSinceLastChangeOfLoadState = 0; +int mainsCyclesSinceLastRF_tx = 0; + + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +volatile int sampleI_grid; +volatile int sampleI_diverted; +volatile int sampleV; + +// for the remote load that is controlled by RF +unsigned long cycleCountAtLastTransmission = 0; +boolean sendRFcommandNextTime = false; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define POLARITY_CHECK_MAXCOUNT 3 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + + + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// With most hardware, the default values are likely to work fine without +// need for change. A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When +// measuring a resistive load, the voltage and current waveforms should be perfectly +// aligned. In this situation, the Power Factor will be 1. +// +// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct +// value of phaseCal for any hardware configuration. An index of my various Mk2-related +// exhibits is available at http://openenergymonitor.org/emon/node/1757 +// +//const float phaseCal_grid = 1.0; <--- not used in this version +//const float phaseCal_diverted = 1.0; <--- not used in this version + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // Energy Diversion Detection (for the local load only) +long requiredExportPerMainsCycle_inIEU; +float IEUtoJoulesConversion_CT1; + + +void setup() +{ + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for the local dump-load + for(int i = 0; i< noOfDumploads; i++) + { + logicalLoadState[i] = LOAD_OFF; + physicalLoadState[i] = LOAD_OFF; + } + + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // force the local load to be in the "off" state. + + pinMode(loadPrioritySelectorPin, INPUT); // this pin is tracked to the "mode" connector + digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and + loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority + + delay(5000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_withRemoteLoad_4b.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, calibration values that are supplied in floating point + // form need to be rescaled. + // +// phaseCal_grid_int = phaseCal_grid * 256; // for integer maths <-- not supported +// phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths <-- not supported + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up + midPointOfEnergyBucket_long = capacityOfEnergyBucket_long / 2; + energyAtLastOffTransition_long = midPointOfEnergyBucket_long; + energyAtLastOnTransition_long = midPointOfEnergyBucket_long; + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the correct scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples. The main processor and +// the ADC work autonomously, their operation being only linked via the dataReady flag. +// As soon as a new set of data is made available by the ADC, the main processor can +// start to work on it immediately. +// +void allGeneralProcessing() +{ + static boolean beyondStartUpPhase = false; // start-up delay, allows things to settle + static boolean triggerNeedsToBeArmed = false; // once per mains cycle (+ve half) +// static int samplesDuringThisMainsCycle; // for normalising the power in each mains cycle + static long sumP_grid; // for per-cycle summation of 'real power' + static long sumP_diverted; // for per-cycle summation of 'real power' +// static enum polarities polarityOfLastSampleV; // for zero-crossing detection + static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) + static long lastSampleVminusDC_long; // for the phaseCal algorithm + static byte perSecondCounter = 0; + + // remove DC offset from the raw voltage sample by subtracting the accurate value + // as determined by a LP filter. + long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long; + + // determine the polarity of the latest voltage sample + enum polarities polarityNow; + if(sampleVminusDC_long > 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + // cycleCount++; <- this mechanism is unsafe, it will eventually overflow + mainsCyclesSinceLastMidPointCrossing++; + mainsCyclesSinceLastChangeOfLoadState++; + mainsCyclesSinceLastRF_tx++; + + // a simple routine for checking the continuity of the sampling scheme + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } + + triggerNeedsToBeArmed = true; // the trigger is armed once during each +ve half-cycle + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / sampleSetsDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- for non-standard use + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + if (EDD_isActive) // Energy Diversion Display + { + // When locally diverted energy is being monitored, the latest contribution + // needs to be added to an accumulator which operates with maximum precision. + // + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) { + realEnergy_diverted = 0; } // to avoid 'creep' + + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + perSecondCounter++; + if(perSecondCounter >= CYCLES_PER_SECOND) + { + perSecondCounter = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); // occurs every second + +// Serial.println (energyInBucket_long * IEUtoJoulesConversion_CT1); + } + + + // when using the single-threshold power diversion algorithm, a counter needs + // to be reset whenever the energy level in the accumulator crosses the mid-point + // + enum energyStates energyStateOnLastLoop = energyStateNow; + + if (energyInBucket_long > midPointOfEnergyBucket_long) { + energyStateNow = UPPER_HALF; } + else { + energyStateNow = LOWER_HALF; } + + if (energyStateNow != energyStateOnLastLoop) { + mainsCyclesSinceLastMidPointCrossing = 0; } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + if (triggerNeedsToBeArmed == true) + { + // check to see whether the trigger device can now be reliably armed + if(sampleSetsDuringThisMainsCycle == 3) // should always exceed 20V (the min for trigger) + { + boolean OKtoSendRFcommandNow = false; + boolean changeOfLoadState = false; + + // a pending RF command takes priority over the normal logic + if (sendRFcommandNextTime) + { + OKtoSendRFcommandNow = true; + sendRFcommandNextTime = false; + } + else + { + /* Now it's time to determine whether any of the the loads need to be changed. + * This is a 2-stage process: + * First, change the LOGICAL loads as necessary, then update the PHYSICAL + * loads according to the mapping that exists between them. The mapping is + * 1:1 by default but can be altered by a hardware switch which allows the + * priority of the remote load to be altered. + * This code uses a single-threshold algorithm which relies on regular switching + * of the load. + */ + if (mainsCyclesSinceLastMidPointCrossing > postMidPointCrossingDelay_cycles) + { + if (energyInBucket_long > midPointOfEnergyBucket_long) + { + increaseLoadIfPossible(); // to reduce the level in the bucket + } + else + { + decreaseLoadIfPossible(); // to increase the level in the bucket + } + } + + + /* Update the state of all physical loads and determine whether the + * state of the remote load has changed. The remote load is hard-coded + * as Physical Load 1 (Physical Load 0 is a local load) + */ + boolean remoteLoadHasChangedState = false; + byte prevStateOfRemoteLoad = (byte)physicalLoadState[1]; + updatePhysicalLoadStates(); + if ((byte)physicalLoadState[1] != prevStateOfRemoteLoad) + { + remoteLoadHasChangedState = true; + } + + /* Now determine whether an RF command should be sent. This can be for + * either of two reasons: + * + * - the on/off state of the remote load needs to be changed + * - a refresh command is due ('cos no change of state has occurred recently) + * + * If the on/off state needs to be changed, but a refresh command was sent on the + * previous cycle, the 'change of state' command is deferred until the next + * cycle. A refresh command can always be sent straight away because it can + * be guaranteed that no command has been sent immediately beforehand. + */ + if (remoteLoadHasChangedState) + { + // ensure that RF commands are not sent on consecutive cycles + if (mainsCyclesSinceLastRF_tx > 1) + { + // the "change of state" can be acted on immediately + OKtoSendRFcommandNow = true; // local flag + } + else + { + // the "change of state" must be deferred until next cycle + sendRFcommandNextTime = true; // global flag + } + } + else + { + // no "change of state", so check whether a refresh command is due + if (mainsCyclesSinceLastRF_tx >= noOfCyclesBeforeRefresh) + { + // a refresh command is due (which can always be sent immediately) + OKtoSendRFcommandNow = true; + } + } + } + + // update the local load, which is physical load 0 + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); + + // update the remote load, which is physical load 1 + if (OKtoSendRFcommandNow) + { + mainsCyclesSinceLastRF_tx = 0; + tx_data.msgNumber = messageNumber++; + tx_data.dumpState = physicalLoadState[1]; +// tx_data.dumpState = messageNumber & 1; // for test only + send_rf_data(); +/* + Serial.print(tx_data.dumpState); + Serial.print(", "); + Serial.print(tx_data.msgNumber); + Serial.print("; "); + Serial.println(activeLoadID); // useful for keeping track of different priority loads +*/ + } + + // update the Energy Diversion Detector + if (physicalLoadState[0] == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + + // clear the flag which ensures that loads are only updated once per mains cycle + triggerNeedsToBeArmed = false; + } + } + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > startUpPeriod * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringThisMainsCycle = 0; + sampleCount_forContinuityChecker = 0; + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + +// checkOutputModeSelection(); // updates outputMode if switch is changed + checkLoadPrioritySelection(); // updates load priorities if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is positive + + // processing for EVERY pair of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform +// long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform +// long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + sampleSetsDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count >= POLARITY_CHECK_MAXCOUNT) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + +void increaseLoadIfPossible() +{ + /* if permitted by A/F rules, turn on the highest priority logical load that is not already on. + */ + boolean changed = false; + + // Only one load may operate freely at a time. Other loads are prevented from + // switching until a sufficient period has elapsed since the last transition. + // This scheme allows a lower priority load to contribute if a higher priority + // load is not having the desired effect, but not immediately. + // + if (energyInBucket_long >= energyAtLastOnTransition_long) + { +// Serial.print('+'); // useful for testing this logic + boolean timeout = (mainsCyclesSinceLastChangeOfLoadState > interLoadSeparationDelay_cycles); + for (int i = 0; i < noOfDumploads && !changed; i++) + { + if (logicalLoadState[i] == LOAD_OFF) + { + if ((i == activeLoadID) || timeout) + { + logicalLoadState[i] = LOAD_ON; + mainsCyclesSinceLastChangeOfLoadState = 0; + energyAtLastOnTransition_long = energyInBucket_long; + energyAtLastOffTransition_long = midPointOfEnergyBucket_long; // reset the 'opposite' mechanism. + activeLoadID = i; + changed = true; + } + } + } + } + else + { + // energy level has not risen so there's no need to apply any more load + } +// return (changed); +} + +void decreaseLoadIfPossible() +{ + /* if permitted by A/F rules, turn off the lowest priority logical load that is not already off. + */ + boolean changed = false; + + // Only one load may operate freely at a time. Other loads are prevented from + // switching until a sufficient period has elapsed since the last transition. + // This scheme allows a lower priority load to contribute if a higher priority + // load is not having the desired effect, but not immediately. + // + if (energyInBucket_long <= energyAtLastOffTransition_long) + { +// Serial.print('-'); // useful for testing this logic + boolean timeout = (mainsCyclesSinceLastChangeOfLoadState > interLoadSeparationDelay_cycles); +// for (int i = 0; i < noOfDumploads && !done; i++) + for (int i = (noOfDumploads -1); i >= 0 && !changed; i--) + { + if (logicalLoadState[i] == LOAD_ON) + { + if ((i == activeLoadID) || timeout) + { + logicalLoadState[i] = LOAD_OFF; + mainsCyclesSinceLastChangeOfLoadState = 0; + energyAtLastOffTransition_long = energyInBucket_long; + energyAtLastOnTransition_long = midPointOfEnergyBucket_long; // reset the 'opposite' mechanism. + activeLoadID = i; + changed = true; + } + } + } + } + else + { + // energy level has not fallen so there's no need to remove any more load + } +// return (changed); +} + + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * By default, the association between the physical and logical loads is 1:1. If + * the remote load is set to have priority, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * - Physical load 0 is local, and is controlled by use of an output pin. + * - Physical load 1 is remote, and is controlled via the associated RFM12B module. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } + + if (loadPriorityMode == REMOTE_HAS_PRIORITY) + { + // swap physical loads 0 & 1 if remote load has priority + physicalLoadState[0] = logicalLoadState[1]; + physicalLoadState[1] = logicalLoadState[0]; + } +} + + +// this function changes the value of the load priorities if the state of the external switch is altered +void checkLoadPrioritySelection() +{ + static byte loadPrioritySwitchCcount = 0; + int pinState = digitalRead(loadPrioritySelectorPin); + if (pinState != loadPriorityMode) + { + loadPrioritySwitchCcount++; + } + if (loadPrioritySwitchCcount >= 20) + { + loadPrioritySwitchCcount = 0; + loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable + Serial.print ("loadPriority selection changed to "); + if (loadPriorityMode == LOCAL_HAS_PRIORITY) { + Serial.println ( "local"); } + else { + Serial.println ( "remote"); } + } +} + + +// Although this sketch always operates in ANTI_FLICKER mode, it was convenient +// to leave this mechanism in place. +// +void configureParamsForSelectedOutputMode() +{ + + if (outputMode == ANTI_FLICKER) + { + postMidPointCrossingDelay_cycles = postMidPointCrossingDelayForAF_cycles; + } + else + { + postMidPointCrossingDelay_cycles = 0; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" midPointOfEnergyBucket_long = "); + Serial.println(midPointOfEnergyBucket_long); + Serial.print(" postMidPointCrossingDelay_cycles = "); + Serial.println(postMidPointCrossingDelay_cycles); + Serial.print(" interLoadSeparationDelay_cycles = "); + Serial.println(interLoadSeparationDelay_cycles); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +// valueToBeDisplayed++; +} + + + + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + +void send_rf_data() +{ + // rf12_sleep(RF12_WAKEUP); + // if ready to send + exit route if it gets stuck + int i = 0; + while (!rf12_canSend() && i<10) + { + rf12_recvDone(); + i++; + } + rf12_sendNow(0, &tx_data, sizeof tx_data); + + // rf12_sendStart(0, &tx_data, sizeof tx_data); + // rf12_sendWait(2); + // rf12_sleep(RF12_SLEEP); +} + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Mk2_withRemoteLoad_5.ino b/docs/routers/mk2pvrouter.co.uk/Mk2_withRemoteLoad_5.ino new file mode 100644 index 0000000..a8a1154 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Mk2_withRemoteLoad_5.ino @@ -0,0 +1,1356 @@ +/* Mk2_RFremoteLoad_5.ino + * + * This sketch is for diverting surplus PV power to a local dump load using a triac + * or a SolidStateRelay. A remote load is also supported, this being controlled + * using the on-board RFM12B module. An external switch allows either load to be + * selected as having the higher priority. If a local load is not provided, all + * surplus power is available for use by the remote load. + * + * This sketch is intended for use with my PCB-based hardware for the Mk2 PV Router. + * The selector switch, as mentioned above, connects to the "mode" port. For this + * version of the Mk2 code, the system uses an alternative version of the anti-flicker + * algorithm which is well suited for multiple loads. As the 'normal' mode is no longer + * required, the 'mode' port can be re-assigned for priority selection. + * + * The integral voltage sensor is fed from one of the secondary coils of the transformer. + * Current is measured via Current Transformers at the CT1 and CT2 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the 'diverted' current, so that energy which is diverted via the + * local dump load can be recorded and displayed locally. + * + * A persistence-based 4-digit display is supported. When the RFM12B module is + * in use, the display can only be used in conjunction with an extra pair of + * logic chips. These are ICs 3 and 4, which reduce the number of processor pins + * that are needed to drive the display. + * + * This sketch has many similarities with Revision 5c of the Mk2i PV Router code that I + * have posted on the OpenEnergyMonitor forum. That version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * September 2014: renamed as Mk2_withRemoteLoad_3, with these changes: + * - reimplementation of cycleCount, as it could have overflowed with unpredictable results; + * - the functions increaseLoadIfPossible() and decreaseLoadIfPossible() have been tidied; + * - energyThreshold_long has been renamed as midPointOfEnergyBucket_long. + * + * December 2014: renamed as Mk2_withRemoteLoad_4, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed); + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances; + * + * January 2016: renamed as Mk2_withRemoteLoad_4a, with these changes: + * - a minor change in the routine timerIsr() has removed a timing uncertainty. + * - support for the RF69 RF module has been added. + * + * January 2016: updated to Mk2_withRemoteLoad_4b: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * February 2016: updated to Mk2_bothDisplays_5, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +// #define RF69_COMPAT 0 // <-- include this line for the RFM12B +#define RF69_COMPAT 1 // <-- include this line for the RF69 + +#include +#include // JeeLib is available at from: http://github.com/jcw/jeelib +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define WORKING_RANGE_IN_JOULES 3600 +#define REFRESH_PERIOD_IN_CYCLES 50 // max allowed interval between RF messages +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +const byte noOfDumploads = 2; // The logic expects a minimum of 2 dumploads, + // for local & remote loads, but neither has to + // be physically present. + +// definitions of enumerated types and instances of same +enum polarities {NEGATIVE, POSITIVE}; +enum outputModes {ANTI_FLICKER, NORMAL}; +enum loadPriorityModes {REMOTE_HAS_PRIORITY, LOCAL_HAS_PRIORITY}; + +enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are active low +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +enum energyStates {LOWER_HALF, UPPER_HALF}; // for single threshold AF algorithm +enum energyStates energyStateNow; + +// ---------------- Extra Features selection ---------------------- +// +// - WORKLOAD_CHECK, for determining how much spare processing time there is. +// +// #define WORKLOAD_CHECK // <-- Include this line is this feature is required + + +// For most single-load Mk2 systems, the power-diversion logic can operate in either of two modes: +// +// - NORMAL, where the load switches rapidly on/off to maintain a constant energy level. +// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations +// of the local mains voltage. +// +// For this multi-load version, the same mechanism has been retained but the +// output mode is hard-coded as below: +enum outputModes outputMode = ANTI_FLICKER; + +// In this multi-load version, the external switch is re-used to determine the load priority +enum loadPriorityModes loadPriorityMode = LOCAL_HAS_PRIORITY; + +/* frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ + */ +#define freq RF12_433MHZ // Use the freq to match the module you have. + +const int nodeID = 10; // RFM12B node ID +const int networkGroup = 210; // RFM12B wireless network group - needs to be same as emonBase and emonGLCD +const int UNO = 1; // Set to 0 if you're not using the UNO bootloader (i.e using Duemilanove) + // - All Atmega's shipped from OpenEnergyMonitor come with Arduino Uno bootloader + +const int noOfCyclesBeforeRefresh = 50; // nominally every second, but not critical +//long cycleCountAtLastRFtransmission = 0; +int messageNumber = 0; + +// data structure for RF comms +typedef struct { byte dumpState; int msgNumber; } Tx_struct; // data for RF comms +Tx_struct tx_data; + + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +// D2 is for the RFM12B +const byte loadPrioritySelectorPin = 3; // <-- this is the "mode" port +const byte physicalLoad_0_pin = 4; // <-- this is the "trigger" port +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +// D10 is for the RFM12B +// D11 is for the RFM12B +// D12 is for the RFM12B +// D13 is for the RFM12B + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + + +const byte delayBeforeSerialStarts = 3; // in seconds, to allow Serial window to be opened +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +long energyInBucket_long = 0; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long midPointOfEnergyBucket_long; // used for 'normal' and single-threshold 'AF' logic +// int phaseCal_grid_int; // to avoid the need for floating-point maths +// int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; + +int postMidPointCrossingDelay_cycles; // assigned in setup(), different for each output mode +const int postMidPointCrossingDelayForAF_cycles = 10; // in 20 ms counts +const int interLoadSeparationDelay_cycles = 25; // in 20 ms cycle counts (for both output modes) +byte activeLoadID; // only one load may operate freely at a time. + +long energyAtLastOffTransition_long; +long energyAtLastOnTransition_long; +int mainsCyclesSinceLastMidPointCrossing = 0; +int mainsCyclesSinceLastChangeOfLoadState = 0; +int mainsCyclesSinceLastRF_tx = 0; + + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +volatile int sampleI_grid; +volatile int sampleI_diverted; +volatile int sampleV; + +// for the remote load that is controlled by RF +unsigned long cycleCountAtLastTransmission = 0; +boolean sendRFcommandNextTime = false; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define PERSISTENCE_FOR_POLARITY_CHANGE 3 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + + + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// With most hardware, the default values are likely to work fine without +// need for change. A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When +// measuring a resistive load, the voltage and current waveforms should be perfectly +// aligned. In this situation, the Power Factor will be 1. +// +// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct +// value of phaseCal for any hardware configuration. An index of my various Mk2-related +// exhibits is available at http://openenergymonitor.org/emon/node/1757 +// +//const float phaseCal_grid = 1.0; <--- not used in this version +//const float phaseCal_diverted = 1.0; <--- not used in this version + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // Energy Diversion Detection (for the local load only) +long requiredExportPerMainsCycle_inIEU; +float IEUtoJoulesConversion_CT1; + + +void setup() +{ + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for the local dump-load + for(int i = 0; i< noOfDumploads; i++) + { + logicalLoadState[i] = LOAD_OFF; + physicalLoadState[i] = LOAD_OFF; + } + + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // force the local load to be in the "off" state. + + pinMode(loadPrioritySelectorPin, INPUT); // this pin is tracked to the "mode" connector + digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and + loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority + + delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_withRemoteLoad_5.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, calibration values that are supplied in floating point + // form need to be rescaled. + // +// phaseCal_grid_int = phaseCal_grid * 256; // for integer maths <-- not supported +// phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths <-- not supported + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + midPointOfEnergyBucket_long = capacityOfEnergyBucket_long / 2; + energyAtLastOffTransition_long = midPointOfEnergyBucket_long; + energyAtLastOnTransition_long = midPointOfEnergyBucket_long; + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the correct scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + IEUtoJoulesConversion_CT1 = powerCal_grid / CYCLES_PER_SECOND; + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + long mainsCyclesPerHour = (long)CYCLES_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples. The main processor and +// the ADC work autonomously, their operation being only linked via the dataReady flag. +// As soon as a new set of data is made available by the ADC, the main processor can +// start to work on it immediately. +// +void allGeneralProcessing() +{ + static boolean beyondStartUpPhase = false; // start-up delay, allows things to settle + static boolean triggerNeedsToBeArmed = false; // once per mains cycle (+ve half) +// static int samplesDuringThisMainsCycle; // for normalising the power in each mains cycle + static long sumP_grid; // for per-cycle summation of 'real power' + static long sumP_diverted; // for per-cycle summation of 'real power' +// static enum polarities polarityOfLastSampleV; // for zero-crossing detection + static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) + static long lastSampleVminusDC_long; // for the phaseCal algorithm + static byte perSecondCounter = 0; + + // remove DC offset from the raw voltage sample by subtracting the accurate value + // as determined by a LP filter. + long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long; + + // determine the polarity of the latest voltage sample + enum polarities polarityNow; + if(sampleVminusDC_long > 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + if (beyondStartUpPhase) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + // cycleCount++; <- this mechanism is unsafe, it will eventually overflow + mainsCyclesSinceLastMidPointCrossing++; + mainsCyclesSinceLastChangeOfLoadState++; + mainsCyclesSinceLastRF_tx++; + + // a simple routine for checking the continuity of the sampling scheme + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } + + triggerNeedsToBeArmed = true; // the trigger is armed once during each +ve half-cycle + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / sampleSetsDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- for non-standard use + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + if (EDD_isActive) // Energy Diversion Display + { + // When locally diverted energy is being monitored, the latest contribution + // needs to be added to an accumulator which operates with maximum precision. + // + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) { + realEnergy_diverted = 0; } // to avoid 'creep' + + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + perSecondCounter++; + if(perSecondCounter >= CYCLES_PER_SECOND) + { + perSecondCounter = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); // occurs every second + +// Serial.println (energyInBucket_long * IEUtoJoulesConversion_CT1); + } + + + // when using the single-threshold power diversion algorithm, a counter needs + // to be reset whenever the energy level in the accumulator crosses the mid-point + // + enum energyStates energyStateOnLastLoop = energyStateNow; + + if (energyInBucket_long > midPointOfEnergyBucket_long) { + energyStateNow = UPPER_HALF; } + else { + energyStateNow = LOWER_HALF; } + + if (energyStateNow != energyStateOnLastLoop) { + mainsCyclesSinceLastMidPointCrossing = 0; } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringThisMainsCycle = 0; // not yet dealt with for this cycle + sampleCount_forContinuityChecker = 1; // opportunity has been missed for this cycle + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + // check to see whether the trigger device can now be reliably armed + if(sampleSetsDuringThisMainsCycle == 3) // much easier than checking the voltage level + { + if (beyondStartUpPhase) + { + boolean OKtoSendRFcommandNow = false; + boolean changeOfLoadState = false; + + // a pending RF command takes priority over the normal logic + if (sendRFcommandNextTime) + { + OKtoSendRFcommandNow = true; + sendRFcommandNextTime = false; + } + else + { + /* Now it's time to determine whether any of the the loads need to be changed. + * This is a 2-stage process: + * First, change the LOGICAL loads as necessary, then update the PHYSICAL + * loads according to the mapping that exists between them. The mapping is + * 1:1 by default but can be altered by a hardware switch which allows the + * priority of the remote load to be altered. + * This code uses a single-threshold algorithm which relies on regular switching + * of the load. + */ + if (mainsCyclesSinceLastMidPointCrossing > postMidPointCrossingDelay_cycles) + { + if (energyInBucket_long > midPointOfEnergyBucket_long) + { + increaseLoadIfPossible(); // to reduce the level in the bucket + } + else + { + decreaseLoadIfPossible(); // to increase the level in the bucket + } + } + + + /* Update the state of all physical loads and determine whether the + * state of the remote load has changed. The remote load is hard-coded + * as Physical Load 1 (Physical Load 0 is a local load) + */ + boolean remoteLoadHasChangedState = false; + byte prevStateOfRemoteLoad = (byte)physicalLoadState[1]; + updatePhysicalLoadStates(); + if ((byte)physicalLoadState[1] != prevStateOfRemoteLoad) + { + remoteLoadHasChangedState = true; + } + + /* Now determine whether an RF command should be sent. This can be for + * either of two reasons: + * + * - the on/off state of the remote load needs to be changed + * - a refresh command is due ('cos no change of state has occurred recently) + * + * If the on/off state needs to be changed, but a refresh command was sent on the + * previous cycle, the 'change of state' command is deferred until the next + * cycle. A refresh command can always be sent straight away because it can + * be guaranteed that no command has been sent immediately beforehand. + */ + if (remoteLoadHasChangedState) + { + // ensure that RF commands are not sent on consecutive cycles + if (mainsCyclesSinceLastRF_tx > 1) + { + // the "change of state" can be acted on immediately + OKtoSendRFcommandNow = true; // local flag + } + else + { + // the "change of state" must be deferred until next cycle + sendRFcommandNextTime = true; // global flag + } + } + else + { + // no "change of state", so check whether a refresh command is due + if (mainsCyclesSinceLastRF_tx >= noOfCyclesBeforeRefresh) + { + // a refresh command is due (which can always be sent immediately) + OKtoSendRFcommandNow = true; + } + } + } + + // update the local load, which is physical load 0 + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); + + // update the remote load, which is physical load 1 + if (OKtoSendRFcommandNow) + { + mainsCyclesSinceLastRF_tx = 0; + tx_data.msgNumber = messageNumber++; + tx_data.dumpState = physicalLoadState[1]; + send_rf_data(); +/* + Serial.print(tx_data.dumpState); + Serial.print(", "); + Serial.print(tx_data.msgNumber); + Serial.print("; "); + Serial.println(activeLoadID); // useful for keeping track of different priority loads +*/ + } + + // update the Energy Diversion Detector + if (physicalLoadState[0] == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + + // clear the flag which ensures that loads are only updated once per mains cycle + triggerNeedsToBeArmed = false; + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12); + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + +// checkOutputModeSelection(); // updates outputMode if switch is changed + checkLoadPrioritySelection(); // updates load priorities if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is negative + + // processing for EVERY pair of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform +// long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform +// long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + sampleSetsDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count > PERSISTENCE_FOR_POLARITY_CHANGE) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} +void increaseLoadIfPossible() +{ + /* if permitted by A/F rules, turn on the highest priority logical load that is not already on. + */ + boolean changed = false; + + // Only one load may operate freely at a time. Other loads are prevented from + // switching until a sufficient period has elapsed since the last transition. + // This scheme allows a lower priority load to contribute if a higher priority + // load is not having the desired effect, but not immediately. + // + if (energyInBucket_long >= energyAtLastOnTransition_long) + { +// Serial.print('+'); // useful for testing this logic + boolean timeout = (mainsCyclesSinceLastChangeOfLoadState > interLoadSeparationDelay_cycles); + for (int i = 0; i < noOfDumploads && !changed; i++) + { + if (logicalLoadState[i] == LOAD_OFF) + { + if ((i == activeLoadID) || timeout) + { + logicalLoadState[i] = LOAD_ON; + mainsCyclesSinceLastChangeOfLoadState = 0; + energyAtLastOnTransition_long = energyInBucket_long; + energyAtLastOffTransition_long = midPointOfEnergyBucket_long; // reset the 'opposite' mechanism. + activeLoadID = i; + changed = true; + } + } + } + } + else + { + // energy level has not risen so there's no need to apply any more load + } +// return (changed); +} + +void decreaseLoadIfPossible() +{ + /* if permitted by A/F rules, turn off the lowest priority logical load that is not already off. + */ + boolean changed = false; + + // Only one load may operate freely at a time. Other loads are prevented from + // switching until a sufficient period has elapsed since the last transition. + // This scheme allows a lower priority load to contribute if a higher priority + // load is not having the desired effect, but not immediately. + // + if (energyInBucket_long <= energyAtLastOffTransition_long) + { +// Serial.print('-'); // useful for testing this logic + boolean timeout = (mainsCyclesSinceLastChangeOfLoadState > interLoadSeparationDelay_cycles); +// for (int i = 0; i < noOfDumploads && !done; i++) + for (int i = (noOfDumploads -1); i >= 0 && !changed; i--) + { + if (logicalLoadState[i] == LOAD_ON) + { + if ((i == activeLoadID) || timeout) + { + logicalLoadState[i] = LOAD_OFF; + mainsCyclesSinceLastChangeOfLoadState = 0; + energyAtLastOffTransition_long = energyInBucket_long; + energyAtLastOnTransition_long = midPointOfEnergyBucket_long; // reset the 'opposite' mechanism. + activeLoadID = i; + changed = true; + } + } + } + } + else + { + // energy level has not fallen so there's no need to remove any more load + } +// return (changed); +} + + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * By default, the association between the physical and logical loads is 1:1. If + * the remote load is set to have priority, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * - Physical load 0 is local, and is controlled by use of an output pin. + * - Physical load 1 is remote, and is controlled via the associated RFM12B module. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } + + if (loadPriorityMode == REMOTE_HAS_PRIORITY) + { + // swap physical loads 0 & 1 if remote load has priority + physicalLoadState[0] = logicalLoadState[1]; + physicalLoadState[1] = logicalLoadState[0]; + } +} + + +// this function changes the value of the load priorities if the state of the external switch is altered +void checkLoadPrioritySelection() +{ + static byte loadPrioritySwitchCcount = 0; + int pinState = digitalRead(loadPrioritySelectorPin); + if (pinState != loadPriorityMode) + { + loadPrioritySwitchCcount++; + } + if (loadPrioritySwitchCcount >= 20) + { + loadPrioritySwitchCcount = 0; + loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable + Serial.print ("loadPriority selection changed to "); + if (loadPriorityMode == LOCAL_HAS_PRIORITY) { + Serial.println ( "local"); } + else { + Serial.println ( "remote"); } + } +} + + +// Although this sketch always operates in ANTI_FLICKER mode, it was convenient +// to leave this mechanism in place. +// +void configureParamsForSelectedOutputMode() +{ + + if (outputMode == ANTI_FLICKER) + { + postMidPointCrossingDelay_cycles = postMidPointCrossingDelayForAF_cycles; + } + else + { + postMidPointCrossingDelay_cycles = 0; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" midPointOfEnergyBucket_long = "); + Serial.println(midPointOfEnergyBucket_long); + Serial.print(" postMidPointCrossingDelay_cycles = "); + Serial.println(postMidPointCrossingDelay_cycles); + Serial.print(" interLoadSeparationDelay_cycles = "); + Serial.println(interLoadSeparationDelay_cycles); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +// valueToBeDisplayed++; +} + + + + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + +void send_rf_data() +{ + // rf12_sleep(RF12_WAKEUP); + // if ready to send + exit route if it gets stuck + int i = 0; + while (!rf12_canSend() && i<10) + { + rf12_recvDone(); + i++; + } + rf12_sendNow(0, &tx_data, sizeof tx_data); + + // rf12_sendStart(0, &tx_data, sizeof tx_data); + // rf12_sendWait(2); + // rf12_sleep(RF12_SLEEP); +} + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/RST_375us_dev.ino b/docs/routers/mk2pvrouter.co.uk/RST_375us_dev.ino new file mode 100644 index 0000000..09e9288 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/RST_375us_dev.ino @@ -0,0 +1,415 @@ +/* February 2014 + * Tool to capture the raw V and I samples generated by the Atmega 328P processor + * during one or more mains cycles. The data is displayed on the Serial Monitor. + * + * Voltage samples are displayed as 'v' + * Current samples via CT1 are displayed as '1' + * + * The display is more compact if not every set of samples is shown. This aspect + * can be changed at the second of the two lines of code which contain a '%' character. + * + * February 2021 + * In the original version, data samples were obtained using the analogRead() function. Now, + * they are obtained by the ADC being controlled by a hardware timer with a periodicity of 125 us, + * hence a full set of 1 x V and 2 x I samples takes 375 us. The same scheme for collecting + * data samples is found in many of my Mk2 PV Router sketches. + * + * When used with an output stage that has zero-crossing detection, the signal at port D4 can + * be used to activate a load for just a single half main cycle. The behaviour of the output signal + * from CT1 can then be studied in detail. + * + * The stream of raw data samples from any floating CT will always be distorted because the CT acts as + * a High Pass Filter. This effect is only noticeable when the current that is being measured changes, + * such as when an electrical load is turned on or off. This sketch includes additional software which + * compensates for this effect. Similar compensation software has been introduced to the varous + * "fasterControl" sketches that now exist. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + * June 2021 + */ + +#include +#include + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum loadStates {LOAD_ON, LOAD_OFF}; // the external trigger device is active low + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) +#define MAINS_CYCLES_PER_SECOND 50 + +const byte outputForTrigger = 4; // active low + +byte sensor_V = 3; +byte sensor_I1 = 5; +byte sensor_I2 = 4; + +long cycleCount = 0; +int samplesRecorded = 0; +const int DCoffsetI1_nominal = 511; // nominal mid-point value of ADC @ x1 scale + +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF + +// extra items for an LPF to improve the processing of data samples from CT1 +long lpf_long = 512; // new LPF, for ofsetting the behaviour of CT1 as a HPF +// +// The next two constants determine the profile of the LPF. +// They are matched to the physical behaviour of the YHDC SCT-013-000 CT +// and the CT1 samples being 375 us apart +// +const float lpf_gain = 8; // <- setting this to 0 disables this extra processing +//const float lpf_gain = 0; // <- setting this to 0 disables this extra processing +const float alpha = 0.002; // + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +volatile int sample_I2; +volatile int sample_I1; +volatile int sample_V; + +enum polarities polarityOfMostRecentVsample; +enum polarities polarityOfLastVsample; +boolean beyondStartUpPhase = false; + +int lastSample_V; // stored value from the previous loop (HP filter is for voltage samples only) +float lastFiltered_V; // voltage values after HP-filtering to remove the DC offset +byte polarityOfLastSample_V; // for zero-crossing detection + +boolean recordingNow; +boolean recordingComplete; +byte cycleNumberBeingRecorded; +byte noOfCyclesToBeRecorded; + +unsigned long recordingMayStartAt; +boolean firstLoop = true; +int settlingDelay = 5; // <<--- settling time (seconds) for HPF + +char blankLine[82]; +char newLine[82]; +int storedSample_V[170]; +int storedSample_I1[170]; +//int storedSample_I2[100]; + +void setup() +{ + + pinMode(outputForTrigger, OUTPUT); + digitalWrite (outputForTrigger, LOAD_OFF); + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: RST_375us_dev.ino"); + // + Serial.print("alpha = "); Serial.println(alpha, 4); + Serial.print("lpf_gain = "); Serial.println(lpf_gain, 1); + Serial.println(); + + // initialise each character of the display line + blankLine[0] = '|'; + blankLine[80] = '|'; + + for (int i = 1; i < 80; i++) { + blankLine[i] = ' '; } + blankLine[40] = '.'; + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + +void timerIsr(void) +{ + static unsigned char sample_index = 0; + static int sample_I2_raw; + static int sample_I1_raw; + + switch(sample_index) + { + case 0: + sample_V = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + sensor_I1; // set up the next conversion, which is for current at CT1 + ADCSRA |= (1<1 !!! + cycleNumberBeingRecorded = 0; + samplesRecorded = 0; + } + + // remove DC offset from the raw voltage sample by subtracting the accurate value + // as determined by a LP filter. + long sample_VminusDC_long = ((long)sample_V<<8) - DCoffset_V_long; + + // determine the polarity of the latest voltage sample + if(sample_VminusDC_long > 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + + if (polarityOfMostRecentVsample == POSITIVE) + { + if (polarityOfLastVsample != POSITIVE) + { + // This is the start of a new mains cycle + cycleCount++; + sampleSetsDuringThisHalfMainsCycle = 0; + + if (recordingNow == true) { + if (cycleNumberBeingRecorded >= noOfCyclesToBeRecorded) { + Serial.print ("No of cycles recorded = "); + Serial.println (cycleNumberBeingRecorded); + dispatch_recorded_data(); } + else { + cycleNumberBeingRecorded++; } } + + else + if((cycleCount % MAINS_CYCLES_PER_SECOND) == 1) { + unsigned long timeNow = millis(); + if (timeNow > recordingMayStartAt) { + recordingNow = true; + cycleNumberBeingRecorded++; } + else { + Serial.println((int)(recordingMayStartAt - timeNow) / 1000); } } + } // end of specific processing for first +ve Vsample in each mains cycle + + // still processing samples where the voltage is POSITIVE ... + // check to see whether the trigger device can now be reliably armed + if ((sampleSetsDuringThisHalfMainsCycle == 3)&& (cycleNumberBeingRecorded == 1)) + { + digitalWrite (outputForTrigger, LOAD_ON); // triac will fire at the next ZC point + } + } // end of specific processing of +ve cycles + else // the polatity of this sample is negative + { + if (polarityOfLastVsample != NEGATIVE) + { + sampleSetsDuringThisHalfMainsCycle = 0; + + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12); + cumVdeltasThisCycle_long = 0; + + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + } // end of processing that is specific to the first Vsample in each -ve half cycle + // still processing samples where the voltage is NEGATIVE ... + // check to see whether the trigger device can now be reliably armed + if ((sampleSetsDuringThisHalfMainsCycle == 3)&& (cycleNumberBeingRecorded == 1)) + { + digitalWrite (outputForTrigger, LOAD_OFF); // triac will release at the next ZC point + } + } // end of processing that is specific to samples where the voltage is negative +// + // processing for EVERY set of samples + // + // extra filtering to offset the HPF effect of CT1 + // + // subtract the nominal DC offset so the data stream is based around zero, as is required + // for the LPF, and left-shift for integer maths use. + long sampleI1minusDC_long = ((long)(sample_I1-DCoffsetI1_nominal))<<8; + + long last_lpf_long = lpf_long; + lpf_long = last_lpf_long + alpha *(sampleI1minusDC_long - last_lpf_long); + sampleI1minusDC_long += (lpf_gain * lpf_long); + + sample_I1 = (sampleI1minusDC_long>>8) + DCoffsetI1_nominal; +// + if (recordingNow == true) + { + storedSample_V[samplesRecorded] = sample_V; + storedSample_I1[samplesRecorded] = sample_I1; +// storedSample_I2[samplesRecorded] = sample_I2; + samplesRecorded++; + } + + sampleSetsDuringThisHalfMainsCycle++; + cumVdeltasThisCycle_long += sample_VminusDC_long; // for use with LP filter + polarityOfLastVsample = polarityOfMostRecentVsample; // for identification of half cycle boundaries +} // end of allGeneralProcessing() + + +void dispatch_recorded_data() +{ + // display raw samples via the Serial Monitor + // ------------------------------------------ + + Serial.print("cycleCount "); + Serial.print(cycleCount); + Serial.print(", samplesRecorded "); + Serial.println(samplesRecorded); + + int V, I1, I2; + int min_V = 1023, min_I1 = 1023, min_I2 = 1023; + int max_V = 0, max_I1 = 0, max_I2 = 0; + + for (int index = 0; index < samplesRecorded; index++) + { + strcpy(newLine, blankLine); + V = storedSample_V[index]; + I1 = storedSample_I1[index]; +// I2 = storedSample_I2[index]; + + if (V < min_V){min_V = V;} + if (V > max_V){max_V = V;} + if (I1 < min_I1){min_I1 = I1;} + if (I1 > max_I1){max_I1 = I1;} +// if (I2 < min_I2){min_I2 = I2;} +// if (I2 > max_I2){max_I2 = I2;} + + newLine[map(V, 0, 1023, 0, 80)] = 'v'; +// newLine[map(I1, 0, 1023, 0, 80)] = '1'; + + int halfRange = 200; + int lowerLimit = 512 - halfRange; + int upperLimit = 512 + halfRange; + if ((I1 > lowerLimit ) && (I1 < upperLimit)) + { + newLine[map(I1, lowerLimit, upperLimit, 0, 80)] = '1'; // <-- raw sample scale + } + +// newLine[map(I2, 0, 1023, 0, 80)] = '2'; + + if ((index % 2) == 0) // change this to "% 1" for full resolution + { + Serial.println(newLine); + } + } + + Serial.print("min_V "); Serial.print(min_V); + Serial.print(", max_V "); Serial.println(max_V); + Serial.print("min_I1 "); Serial.print(min_I1); + Serial.print(", max_I1 "); Serial.println(max_I1); + Serial.print("min_I2 "); Serial.print(min_I2); + Serial.print(", max_I2 "); Serial.println(max_I2); + + Serial.println(); + + // despatch raw samples via the Serial Monitor + // ------------------------------------------- + /* + Serial.println("Raw data from stored cycle: , , [cr]"); + Serial.print(samplesRecorded); + Serial.println(", <<< No of sample sets"); + + for (int index = 0; index < samplesRecorded; index++) + { + Serial.print (storedSample_V[index]); + Serial.print(", "); + Serial.println (storedSample_I1[index]); +// Serial.print(", "); +// Serial.println (storedSample_I2[index]); + } + */ + recordingNow = false; + firstLoop = true; + pause(); +} + +void pause() +{ + byte done = false; + byte dummyByte; + + while (done != true) + { + if (Serial.available() > 0) + { + dummyByte = Serial.read(); // to 'consume' the incoming byte + if (dummyByte == 'g') done++; + } + } +} + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} diff --git a/docs/routers/mk2pvrouter.co.uk/RSTresults_V_and_I2.txt b/docs/routers/mk2pvrouter.co.uk/RSTresults_V_and_I2.txt new file mode 100644 index 0000000..831e08e --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/RSTresults_V_and_I2.txt @@ -0,0 +1,120 @@ +Results file: RSTresults_V_and_I2.txt +Sketch: RawSamplesTool_2chan.ino +Date: 21-Feb-2014 + +This set of results was taken using a rev 1.1 PCB for my +Mk2 PV Router project. The voltage sensor is built into the +hardware. The mains supply is 240V AC, 50 Hz. + +A standard YHDC SCT-13 CT was attached to the CT2 port, and +the current taken by a 3 kW resistive load was being measured. +Nothing was connected to the CT1 port. + +The burden resistor for each of the CT ports is 150R, and there +is an on-board 3.3V DC power supply for the Atmega 328 processor. + + +Robin Emley +www.Mk2PVrouter.co.uk + +printout as copied from the Serial Monitor window: + +------------------------------------- +Sketch ID: RawSamplesTool_2chan.ino + +>>free RAM = 712 +millis() now = 5039 +recordingMayStartAt 10039 +4 +3 +2 +1 +0 +No of cycles recorded = 1 +cycleCount 252, samplesRecorded 53 +| 2 1.v | +| 2 1. v | +| 2 1. v | +| 2 1. v | +| 2 1. v | +| 2 1. v | +| 2 1. v | +| 2 1. v | +| 2 1. v | +| 2 1. v | +| 2 1. v | +| 2 1. v | +| 21. v | +| 1. 2 | +| v 1. 2 | +| v 1. 2 | +| v 1. 2 | +| v 1. 2 | +| v 1. 2 | +| v 1. 2 | +| v 1. 2 | +| v 1. 2 | +| v 1. 2 | +| v 1. 2 | +| v 1. 2 | +| v 1. 2 | +| 2 v1. | +min_V 181, max_V 835 +min_I1 507, max_I1 508 +min_I2 99, max_I2 918 + +Raw data from stored cycle: , , [cr] +53, <<< No of sample sets +526, 507, 411 +566, 507, 357 +600, 507, 313 +633, 508, 273 +661, 507, 239 +690, 508, 206 +717, 507, 180 +743, 508, 150 +773, 507, 126 +796, 507, 108 +810, 507, 100 +821, 507, 99 +829, 507, 106 +832, 507, 113 +834, 507, 122 +835, 507, 132 +834, 507, 144 +830, 507, 178 +797, 507, 221 +765, 507, 264 +729, 508, 308 +696, 507, 352 +657, 508, 401 +616, 507, 444 +580, 507, 487 +542, 507, 538 +500, 507, 588 +465, 507, 636 +431, 508, 683 +398, 507, 724 +368, 507, 761 +339, 507, 795 +310, 507, 823 +287, 507, 851 +257, 507, 879 +230, 508, 900 +210, 507, 912 +199, 507, 918 +190, 507, 913 +185, 507, 906 +183, 507, 897 +181, 507, 888 +181, 507, 877 +181, 507, 857 +204, 507, 814 +234, 508, 772 +267, 508, 728 +303, 507, 683 +341, 508, 636 +381, 507, 590 +419, 507, 548 +455, 507, 500 +494, 507, 450 diff --git a/docs/routers/mk2pvrouter.co.uk/RawSamplesTool_2chan.ino b/docs/routers/mk2pvrouter.co.uk/RawSamplesTool_2chan.ino new file mode 100644 index 0000000..332c506 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/RawSamplesTool_2chan.ino @@ -0,0 +1,257 @@ +/* + * Tool to capture the raw samples generated by the Atmega 328P processor + * during one or more mains cycles. The data is displayed on the Serial Monitor, + * and is also available for subsequent processing using a spreadsheet. + * + * This version is based on similar code that I posted in December 2012 on the + * OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * The pin-allocations have been changed to suit my PCB-based hardware for the + * Mk2 PV Router. The integral voltage sensor is fed from one of the secondary + * coils of the transformer. Current can be measured via Current Transformers + * at the CT1 and CT1 ports. + * + * Voltage samples are displayed as 'v' + * Current samples via CT1 are displayed as '1' + * Current samples via CT2 are displayed as '2' + * + * The display is more compact if not every set of samples is shown. This aspect + * can be changed at the second of the two lines which contain a '%' character. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + * February 2014 + */ + +#define POSITIVE 1 +#define NEGATIVE 0 +#define CYCLES_PER_SECOND 50 + +byte sensorPin_V = 3; +byte sensorPin_I1 = 5; +byte sensorPin_I2 = 4; + +long cycleCount = 0; +int samplesRecorded = 0; + +byte polarityNow; +boolean beyondStartUpPhase = false; + +int lastSample_V; // stored value from the previous loop (HP filter is for voltage samples only) +float lastFiltered_V; // voltage values after HP-filtering to remove the DC offset +byte polarityOfLastSample_V; // for zero-crossing detection + +boolean recordingNow; +boolean recordingComplete; +byte cycleNumberBeingRecorded; +byte noOfCyclesToBeRecorded; + +unsigned long recordingMayStartAt; +boolean firstLoop = true; +int settlingDelay = 5; // <<--- settling time (seconds) for HPF + +char blankLine[82]; +char newLine[82]; +int storedSample_V[100]; +int storedSample_I1[100]; +int storedSample_I2[100]; + +void setup() +{ + delay(5000); // allow time for the Serial Window to be opened + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: RawSamplesTool_2chan.ino"); + Serial.println(); + + // initialise each character of the display line + blankLine[0] = '|'; + blankLine[80] = '|'; + + for (int i = 1; i < 80; i++) { + blankLine[i] = ' '; } + blankLine[40] = '.'; + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + + +/* Allow the system to run for several seconds so that the filtered + * voltage waveform can settle down. This info is needed for determining + * the start of each new mains cycle. During this period, a countdown + * is displayed. + * + * After the settling period has expired, raw samples taken during + * one complete mains cycle are stored in an array. The capacity of the + * array needs to be sufficient for the number of sample pairs that may + * appear. + * + * At the start of the following cycle, the data collected during the + * previous cycle data is sent to the Serial window. + */ +void loop() // each iteration of loop is for one set of measurements only +{ + if(firstLoop) + { + unsigned long timeNow = millis(); + Serial.print ("millis() now = "); + Serial.println (timeNow); + + recordingMayStartAt = timeNow + (settlingDelay * 1000); + Serial.print ("recordingMayStartAt "); + Serial.println (recordingMayStartAt); + + recordingNow = false; + firstLoop = false; + recordingComplete = false; + noOfCyclesToBeRecorded = 1; // more array space may be needed if this value is >1 !!! + cycleNumberBeingRecorded = 0; + samplesRecorded = 0; + } + + int sample_V = analogRead(sensorPin_V); // from the inbuilt voltage sensor + int sample_I1 = analogRead(sensorPin_I1); // from CT1 + int sample_I2 = analogRead(sensorPin_I2); // from CT + float filtered_V = 0.996*(lastFiltered_V + sample_V - lastSample_V); + + byte polarityOfThisSample_V; + if(filtered_V > 0) + { + polarityOfThisSample_V = POSITIVE; + + if (polarityOfLastSample_V != POSITIVE) + { + // This is the start of a new mains cycle + cycleCount++; + + if (recordingNow == true) { + if (cycleNumberBeingRecorded >= noOfCyclesToBeRecorded) { + Serial.print ("No of cycles recorded = "); + Serial.println (cycleNumberBeingRecorded); + dispatch_recorded_data(); } + else { + cycleNumberBeingRecorded++; } } + + else + if((cycleCount % CYCLES_PER_SECOND) == 1) { + unsigned long timeNow = millis(); + if (timeNow > recordingMayStartAt) { + recordingNow = true; + cycleNumberBeingRecorded++; } + else { + Serial.println((int)(recordingMayStartAt - timeNow) / 1000); } } + } // end of specific processing for first +ve reading in each mains cycle + + } // end of specific processing of +ve cycles + else + { + polarityOfThisSample_V = NEGATIVE; + } + + if (recordingNow == true) + { + storedSample_V[samplesRecorded] = sample_V; + storedSample_I1[samplesRecorded] = sample_I1; + storedSample_I2[samplesRecorded] = sample_I2; + samplesRecorded++; + } + + polarityOfLastSample_V = polarityOfThisSample_V; + lastSample_V = sample_V; + lastFiltered_V = filtered_V; +} // end of loop() + + +void dispatch_recorded_data() +{ + // display raw samples via the Serial Monitor + // ------------------------------------------ + + Serial.print("cycleCount "); + Serial.print(cycleCount); + Serial.print(", samplesRecorded "); + Serial.println(samplesRecorded); + + int V, I1, I2; + int min_V = 1023, min_I1 = 1023, min_I2 = 1023; + int max_V = 0, max_I1 = 0, max_I2 = 0; + + for (int index = 0; index < samplesRecorded; index++) + { + strcpy(newLine, blankLine); + V = storedSample_V[index]; + I1 = storedSample_I1[index]; + I2 = storedSample_I2[index]; + + if (V < min_V){min_V = V;} + if (V > max_V){max_V = V;} + if (I1 < min_I1){min_I1 = I1;} + if (I1 > max_I1){max_I1 = I1;} + if (I2 < min_I2){min_I2 = I2;} + if (I2 > max_I2){max_I2 = I2;} + + newLine[map(V, 0, 1023, 0, 80)] = 'v'; + newLine[map(I1, 0, 1023, 0, 80)] = '1'; + newLine[map(I2, 0, 1023, 0, 80)] = '2'; + + if ((index % 2) == 0) // change this to "% 1" for full resolution + { + Serial.println(newLine); + } + } + + Serial.print("min_V "); Serial.print(min_V); + Serial.print(", max_V "); Serial.println(max_V); + Serial.print("min_I1 "); Serial.print(min_I1); + Serial.print(", max_I1 "); Serial.println(max_I1); + Serial.print("min_I2 "); Serial.print(min_I2); + Serial.print(", max_I2 "); Serial.println(max_I2); + + Serial.println(); + + // despatch raw samples via the Serial Monitor + // ------------------------------------------- + + Serial.println("Raw data from stored cycle: , , [cr]"); + Serial.print(samplesRecorded); + Serial.println(", <<< No of sample sets"); + + for (int index = 0; index < samplesRecorded; index++) + { + Serial.print (storedSample_V[index]); + Serial.print(", "); + Serial.print (storedSample_I1[index]); + Serial.print(", "); + Serial.println (storedSample_I2[index]); + } + + recordingNow = false; + firstLoop = true; + pause(); +} + +void pause() +{ + byte done = false; + byte dummyByte; + + while (done != true) + { + if (Serial.available() > 0) + { + dummyByte = Serial.read(); // to 'consume' the incoming byte + if (dummyByte == 'g') done++; + } + } +} + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + diff --git a/docs/routers/mk2pvrouter.co.uk/RawSamplesTool_6chan.ino b/docs/routers/mk2pvrouter.co.uk/RawSamplesTool_6chan.ino new file mode 100644 index 0000000..b5b5a24 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/RawSamplesTool_6chan.ino @@ -0,0 +1,301 @@ +/* + * Tool to capture the raw samples generated by an Arduino during several mains + * cycles. This data is displayed on the Serial Monitor, and is also available + * for subsequent processing using a spreadsheet. + * + * Pauses after each set of measurements has been taken. Press 'g', then [cr], + * to repeat. + * + * Robin Emley (calypso_rae on Open Energy Monitor Forum) + * December 2012 + */ + +#define POSITIVE 1 +#define NEGATIVE 0 +#define ON 0 // the external trigger device is active low +#define OFF 1 + +/* +byte sensorPin_V1 = 0; +byte sensorPin_V3 = 1; +byte sensorPin_I1 = 2; +byte sensorPin_V2 = 3; +byte sensorPin_I2 = 4; +byte sensorPin_I3 = 5; +*/ + +byte sensorPin_V1 = 0; +byte sensorPin_I1 = 1; +byte sensorPin_V2 = 2; +byte sensorPin_I2 = 3; +byte sensorPin_V3 = 4; +byte sensorPin_I3 = 5; + +long cycleCount = 0; +int samplesRecorded = 0; +float cyclesPerSecond = 50; // use float to ensure accurate maths + +byte polarityNow; +boolean beyondStartUpPhase = false; +byte currentStateOfTriac; + +int lastSample_V1; // stored value from the previous loop (HP filter is for voltage samples only) +float lastFiltered_V1; // voltage values after HP-filtering to remove the DC offset +byte polarityOfLastSample_V1; // for zero-crossing detection + +boolean recordingNow; +boolean recordingComplete; +byte cycleNumberBeingRecorded; +byte noOfCyclesToBeRecorded; + +unsigned long recordingMayStartAt; +boolean firstLoop = true; +int settlingDelay = 5; // <<--- settling time (seconds) for HPF + +char blankLine[82]; +char newLine[82]; +int storedSample_V1[50]; +int storedSample_V2[50]; +int storedSample_V3[50]; +int storedSample_I1[50]; +int storedSample_I2[50]; +int storedSample_I3[50]; + +void setup() +{ + delay(3000); + Serial.begin(9600); + + // initialise each character of the display line + blankLine[0] = '|'; + blankLine[80] = '|'; + + for (int i = 1; i < 80; i++) + { + blankLine[i] = ' '; + } + + blankLine[40] = '.'; + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + +} + + +/* Allow the system to run for several seconds so that the filtered + * voltage waveform can settle down. This info is needed for determining + * the start of each new mains cycle. During this period, a countdown + * is displayed. + * + * After the settling period has expired, raw samples taken during + * one complete mains cycle are stored in an array. The capacity of the + * array needs to be sufficient for the number of sample pairs that may + * appear. A 100 x 2 integer array will probably suffice. + * + * At the start of the following cycle, the data collected during the + * previous cycle data is sent to the Serial window. + */ +void loop() // each iteration of loop is for one pair of measurements only +{ + if(firstLoop) + { + unsigned long timeNow = millis(); + Serial.print ("millis() now = "); + Serial.println (timeNow); + + recordingMayStartAt = timeNow + (settlingDelay * 1000); + Serial.print ("recordingMayStartAt "); + Serial.println (recordingMayStartAt); + + recordingNow = false; + firstLoop = false; + recordingComplete = false; + noOfCyclesToBeRecorded = 1; + cycleNumberBeingRecorded = 0; + samplesRecorded = 0; + } + + int sample_V1 = analogRead(sensorPin_V1); //Read in raw voltage signal + int sample_I1 = analogRead(sensorPin_I1); //Read in raw current signal + int sample_V2 = analogRead(sensorPin_V2); //Read in raw voltage signal + int sample_I2 = analogRead(sensorPin_I2); //Read in raw current signal + int sample_V3 = analogRead(sensorPin_V3); //Read in raw current signal + int sample_I3 = analogRead(sensorPin_I3); //Read in raw current signal + + float filtered_V1 = 0.996*(lastFiltered_V1 + sample_V1 - lastSample_V1); + + byte polarityOfThisSample_V1; + if(filtered_V1 > 0) + { + polarityOfThisSample_V1 = POSITIVE; + + if (polarityOfLastSample_V1 != POSITIVE) + { + // This is the start of a new mains cycle + cycleCount++; + + if (recordingNow == true) { + if (cycleNumberBeingRecorded >= noOfCyclesToBeRecorded) { + Serial.print ("No of cycles recorded = "); + Serial.println (cycleNumberBeingRecorded); + dispatch_recorded_data(); } + else { + cycleNumberBeingRecorded++; } } + + else + if((cycleCount % 50) == 1) { + unsigned long timeNow = millis(); + if (timeNow > recordingMayStartAt) { + recordingNow = true; + cycleNumberBeingRecorded++; } + else { + Serial.println((int)(recordingMayStartAt - timeNow) / 1000); } } + } // end of specific processing for first +ve reading in each mains cycle + + } // end of specific processing of +ve cycles + else + { + polarityOfThisSample_V1 = NEGATIVE; + + if (polarityOfLastSample_V1 != NEGATIVE) + { + // at the start of a new negative half cycle + } + } + + if (recordingNow == true) + { + storedSample_V1[samplesRecorded] = sample_V1; + storedSample_V2[samplesRecorded] = sample_V2; + storedSample_V3[samplesRecorded] = sample_V3; + storedSample_I1[samplesRecorded] = sample_I1; + storedSample_I2[samplesRecorded] = sample_I2; + storedSample_I3[samplesRecorded] = sample_I3; + samplesRecorded++; + } + + polarityOfLastSample_V1 = polarityOfThisSample_V1; + lastSample_V1 = sample_V1; + lastFiltered_V1 = filtered_V1; +} // end of loop() + + +void dispatch_recorded_data() +{ + // display raw samples via the Serial Monitor + // ------------------------------------------ + + Serial.print("cycleCount "); + Serial.print(cycleCount); + Serial.print(", samplesRecorded "); + Serial.println(samplesRecorded); + + int V1, I1; + int min_V1 = 1023, min_I1 = 1023; + int max_V1 = 0, max_I1 = 0; + int V2, I2; + int min_V2 = 1023, min_I2 = 1023; + int max_V2 = 0, max_I2 = 0; + int V3, I3; + int min_V3 = 1023, min_I3 = 1023; + int max_V3 = 0, max_I3 = 0; + + for (int index = 0; index < samplesRecorded; index++) + { + strcpy(newLine, blankLine); + V1 = storedSample_V1[index]; + I1 = storedSample_I1[index]; + V2 = storedSample_V2[index]; + I2 = storedSample_I2[index]; + V3 = storedSample_V3[index]; + I3 = storedSample_I3[index]; + + if (V1 < min_V1){min_V1 = V1;} + if (V1 > max_V1){max_V1 = V1;} + if (I1 < min_I1){min_I1 = I1;} + if (I1 > max_I1){max_I1 = I1;} + + if (V2 < min_V2){min_V2 = V2;} + if (V2 > max_V2){max_V2 = V2;} + if (I2 < min_I2){min_I2 = I2;} + if (I2 > max_I2){max_I2 = I2;} + + if (V3 < min_V3){min_V3 = V3;} + if (V3 > max_V3){max_V3 = V3;} + if (I3 < min_I3){min_I3 = I3;} + if (I3 > max_I3){max_I3 = I3;} + + newLine[map(V1, 0, 1023, 0, 80)] = '0'; + newLine[map(I1, 0, 1023, 0, 80)] = '1'; + newLine[map(V2, 0, 1023, 0, 80)] = '2'; + newLine[map(I2, 0, 1023, 0, 80)] = '3'; + newLine[map(V3, 0, 1023, 0, 80)] = '4'; + newLine[map(I3, 0, 1023, 0, 80)] = '5'; + + if ((index % 1) == 0) // change this to "% 1" for full resolution + { + Serial.println(newLine); + } + } + + Serial.print("min_V1 "); Serial.print(min_V1); + Serial.print(", max_V1 "); Serial.print(max_V1); + Serial.print(", min_I1 "); Serial.print(min_I1); + Serial.print(", max_I1 "); Serial.println(max_I1); + + Serial.print("min_V2 "); Serial.print(min_V2); + Serial.print(", max_V2 "); Serial.print(max_V2); + Serial.print(", min_I2 "); Serial.print(min_I2); + Serial.print(", max_I2 "); Serial.println(max_I2); + + Serial.print("min_V3 "); Serial.print(min_V3); + Serial.print(", max_V3 "); Serial.print(max_V3); + Serial.print(", min_I3 "); Serial.print(min_I3); + Serial.print(", max_I3 "); Serial.println(max_I3); + + + Serial.println(); + Serial.println(); + + // despatch raw samples via the Serial Monitor + // ------------------------------------------- + + Serial.println("Raw data from stored cycle: ,[cr]"); + Serial.print(samplesRecorded); + Serial.println(", <<< No of sample pairs"); + + for (int index = 0; index < samplesRecorded; index++) + { + Serial.print (storedSample_V1[index]); + Serial.print(','); + Serial.println (storedSample_I1[index]); + } + + recordingNow = false; + firstLoop = true; + pause(); +} + +void pause() +{ + byte done = false; + byte dummyByte; + + while (done != true) + { + if (Serial.available() > 0) + { + dummyByte = Serial.read(); // to 'consume' the incoming byte + if (dummyByte == 'g') done++; + } + } +} + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + diff --git a/docs/routers/mk2pvrouter.co.uk/Rev_4.1_circuit.pdf b/docs/routers/mk2pvrouter.co.uk/Rev_4.1_circuit.pdf new file mode 100644 index 0000000..95df68a Binary files /dev/null and b/docs/routers/mk2pvrouter.co.uk/Rev_4.1_circuit.pdf differ diff --git a/docs/routers/mk2pvrouter.co.uk/Transformer_Checker.ino b/docs/routers/mk2pvrouter.co.uk/Transformer_Checker.ino new file mode 100644 index 0000000..3e32c47 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/Transformer_Checker.ino @@ -0,0 +1,328 @@ +/* Transformer_Checker is based on Mk2_RF_datalog_3.ino + * + * Every 1-phase Mk2 PV Router control board has a mains transformer with two secondary outputs. One output provides + * a low-voltage replica of the AC mains voltage; the other is rectified to provide a low-voltage DC supply for the + * processor. Although the power consumption of the Atmel 328P processor is fairly constant, it will be increase + * whenever the output stage is activated. The increased draw from the DC supply will cause the amplitude of the AC signal + * from the other output to slightly decrease. + * + * This sketch can be used to quantify the above effect. A standard output stage should be connected to the primary + * output port but no AC load should be connected otherwise a consequent reduction in the local mains voltage + * could adversely affect this test. + * + * Via the Serial Monitor, this sketch will display the percentage reduction in the measured Vrms value whenever + * the output stage is activated. By adding an extra LED which operates in anti-phase with the primary output, the + * reduction in Vrms can be effectively eliminated. Both LEDs can be driven by the same output port but with their other + * terminals connected to opposite power rails via series resistors of appropriate values. + * + * Any reduction in the measured Vrms value when the output stage is activated represents a non-linearity which will + * result in less than ideal performance. By means of this sketch, an extra LED and series resistor can be used to + * minimise any such effect. + * + * July 2021: first release. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define DATALOG_PERIOD 5 // seconds + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum outputStates {OUTPUT_STAGE_OFF, OUTPUT_STAGE_ON}; +enum outputStates nextOutputState = OUTPUT_STAGE_ON; +enum outputStates outputStateNow; + +int outputStateCounter = 0; +float Vrms_whileOutputStageIsOn; +float Vrms_whileOutputStageIsOff; +#define MAX_CONSECUTIVE_CYCLES 10 + +// allocation of digital pins +// ************************** +const byte outputForTrigger = 4; + +// allocation of analogue pins +// *************************** +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 (which is not used by this sketch) +const byte currentSensor_grid = 5; // A5 is for CT1 (which is not used by this sketch) + +const byte startUpPeriod = 1; // in seconds, to allow LP filter to settle + +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF + +long sum_Vsquared_whileOutputStageIsOn; +long sum_Vsquared_whileOutputStageIsOff; + +long sampleSets_whileOutputStageIsOn; +long sampleSets_whileOutputStageIsOff; + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +int sampleI_grid; +int sampleI_diverted; +int sampleV; + + +// Calibration values +//------------------- +// When operating at 230 V AC, the range of ADC values will be similar to the actual range of volts, +// so the optimal value for this cal factor will be close to unity. For this sketch, the value of voltageCal +// makes no difference because the key output is a ratio between the results of two calculations which both +// use the same voltageCal value. +// +const float voltageCal = 1.0; + + +void setup() +{ + pinMode(outputForTrigger, OUTPUT); + digitalWrite (outputForTrigger, OUTPUT_STAGE_ON); + + delay(1000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Transformer_Checker.ino"); + Serial.println(); + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + Serial.println ("----"); +} + +// This Interrupt Service Routine is for use when the ADC is fixed timer mode. It is +// executed whenever the ADC timer expires. In this mode, the next ADC conversion is +// initiated from within this ISR. +// +void timerIsr(void) +{ + static unsigned char sample_index = 0; + + switch(sample_index) + { + case 0: + sampleV = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current + ADCSRA |= (1< 0) { + polarityNow = POSITIVE; } + else { + polarityNow = NEGATIVE; } + + if (polarityNow == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + outputStateNow = nextOutputState; // to correspond with the action of the opto-isolator + + perSecondCounter++; + if(perSecondCounter >= CYCLES_PER_SECOND) + { + perSecondCounter = 0; + + // routine data is calculated every N seconds + datalog_counter++; + if (datalog_counter >= DATALOG_PERIOD) + { + datalog_counter = 0; + + Vrms_whileOutputStageIsOn = + voltageCal * sqrt(sum_Vsquared_whileOutputStageIsOn / sampleSets_whileOutputStageIsOn); + Vrms_whileOutputStageIsOff = + voltageCal * sqrt(sum_Vsquared_whileOutputStageIsOff / sampleSets_whileOutputStageIsOff); + + sum_Vsquared_whileOutputStageIsOn = 0; + sampleSets_whileOutputStageIsOn = 0; + sum_Vsquared_whileOutputStageIsOff = 0; + sampleSets_whileOutputStageIsOff = 0; + + Serial.print(Vrms_whileOutputStageIsOff); + Serial.print(", "); + Serial.print(Vrms_whileOutputStageIsOn); + Serial.print(", "); + float Vrms_reduction = (Vrms_whileOutputStageIsOn / Vrms_whileOutputStageIsOff) * 100; + Serial.print(Vrms_reduction, 2); + Serial.println('%'); + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > startUpPeriod * 1000) + { + beyondStartUpPhase = true; + Serial.println ("Go!"); + } + } + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + sampleSetsDuringNegativeHalfOfMainsCycle = 0; + + outputStateCounter++; + if(outputStateCounter >= MAX_CONSECUTIVE_CYCLES) + { + outputStateCounter = 0; + nextOutputState = (enum outputStates)!outputStateNow; + } + } // end of processing that is specific to the first Vsample in each -ve half cycle + + sampleSetsDuringNegativeHalfOfMainsCycle++; + + // check to see whether the trigger device can now be reliably armed + if (sampleSetsDuringNegativeHalfOfMainsCycle == 3) + { + digitalWrite(outputForTrigger, !nextOutputState); // the trigger control circuit is active low + } + + } // end of processing that is specific to samples where the voltage is negative + + // processing for EVERY pair of samples + // + // for the Vrms calculations + long filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + long inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (now x4096, or 2^12) + inst_Vsquared = inst_Vsquared>>12; // scaling is now x1 (V_ADC x I_ADC) + + if (outputStateNow == OUTPUT_STAGE_ON) + { + sum_Vsquared_whileOutputStageIsOn += inst_Vsquared; + sampleSets_whileOutputStageIsOn++; + } + else + { + sum_Vsquared_whileOutputStageIsOff += inst_Vsquared; + sampleSets_whileOutputStageIsOff++; + } + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + polarityOfLastSampleV = polarityNow; // for identification of half cycle boundaries +} + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/Wiring_schematic_for_Mk2_Router.pdf b/docs/routers/mk2pvrouter.co.uk/Wiring_schematic_for_Mk2_Router.pdf new file mode 100644 index 0000000..4f45b92 Binary files /dev/null and b/docs/routers/mk2pvrouter.co.uk/Wiring_schematic_for_Mk2_Router.pdf differ diff --git a/docs/routers/mk2pvrouter.co.uk/balanceCheck_1kW.ino b/docs/routers/mk2pvrouter.co.uk/balanceCheck_1kW.ino new file mode 100644 index 0000000..9a0b1b3 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/balanceCheck_1kW.ino @@ -0,0 +1,1086 @@ +/* balanceCheck_1kW.ino + * + * This sketch is based on the standard Mk2 code for my mature product. + * 1 kW of surplus power is synthesised to falicitate a simple balance test + * without the need for a second load to act as a PV generator. + * With a small load, the output should remain on; with a load greater + * than 1kW, it should cycle on/off in accordance with the "mode" switch + * setting. + * + * RAE, April 2014 + */ + +#include + +#include +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define SWEETZONE_IN_JOULES 3600 +#define SURPLUS_PV_IN_WATTS 1000 + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +// The two versions of the hardware require different logic. The following line should +// be included if the additional logic chips are present, or excluded if they are +// absent (in which case some wire links need to be fitted) +// +//#define PIN_SAVING_HARDWARE + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum triacStates {TRIAC_ON, TRIAC_OFF}; // the external trigger device is active low +enum outputModes {ANTI_FLICKER, NORMAL}; + +// ---------------- Extra Features selection ---------------------- +// +// - WORKLOAD_CHECK, for determining how much spare processing time there is. +// +// #define WORKLOAD_CHECK // <-- Include this line is this feature is required + + +// The power-diversion logic can operate in either of two modes: +// +// - NORMAL, where the triac switches rapidly on/off to maintain a constant energy level. +// - ANTI_FLICKER, whereby the repetition rate is reduced to avoid rapid fluctuations +// of the local mains voltage. +// +// The output mode is determined in realtime via a selector switch +enum outputModes outputMode; + +// allocation of digital pins for prototype PCB-based rig (with simple display adapter) +// ****************************************************** +// D0 & D1 are reserved for the Serial i/f +// D2 is a driver line for the 4-digit display (segment D, via series resistor) +const byte outputModeSelectorPin = 3; // <-- with the internal pullup +const byte outputForTrigger = 4; +// D5 is a driver line for the 4-digit display (segment B, via series resistor) +// D6 is a driver line for the 4-digit display (digit 3, via wire link) +// D7 is a driver line for the 4-digit display (digit 2, via wire link) +// D8 is a driver line for the 4-digit display (segment F, via series resistor) +// D9 is a driver line for the 4-digit display (segment A, via series resistor) +// D10 is a driver line for the 4-digit display (segment DP, via series resistor) +// D11 is a driver line for the 4-digit display (segment C, via series resistor) +// D12 is a driver line for the 4-digit display (segment G, via series resistor) +// D13 is a driver line for the 4-digit display (digit 4, via wire link) + +// allocation of analogue pins +// *************************** +// A0 (D14) is a driver line for the 4-digit display (digit 1, via wire link) +// A1 (D15) is a driver line for the 4-digit display (segment E, via series resistor) +// A2 (D16) is unused (it's routed to pin 1 of IC4 which is not fitted) +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + + +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long cycleCount = 0; // counts mains cycles from start-up +long triggerThreshold_long; // for determining when the trigger may be safely armed +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long lowerEnergyThreshold_long; // for turning triac off +long upperEnergyThreshold_long; // for turning triac on +// int phaseCal_grid_int; // to avoid the need for floating-point maths +// int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; +long mainsCyclesPerHour; + +// this setting is only used if anti-flicker mode is enabled +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.5 + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +int sampleI_grid; +int sampleI_diverted; +int sampleV; + + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// With most hardware, the default values are likely to work fine without +// need for change. A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When +// measuring a resistive load, the voltage and current waveforms should be perfectly +// aligned. In this situation, the Power Factor will be 1. +// +// My sketch "PhasecalChecker.ino" provides an easy way to determine the correct +// value of phaseCal for any hardware configuration. An index of my various Mk2-related +// exhibits is available at http://openenergymonitor.org/emon/node/1757 +// +//const float phaseCal_grid = 1.0; <--- not used in this version +//const float phaseCal_diverted = 1.0; <--- not used in this version + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define UPDATE_PERIOD_FOR_DISPLAYED_DATA 50 // mains cycles +#define DISPLAY_SHUTDOWN_IN_HOURS 10 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +// The two versions of the hardware require different logic. +#ifdef PIN_SAVING_HARDWARE + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. In this version, +// the decimal point has to be treated differently than the other seven segments, so +// a convenient means of accessing this column is provided. +// +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + +#else // PIN_SAVING_HARDWARE + +#define ON HIGH +#define OFF LOW + +const byte noOfSegmentsPerDigit = 8; // includes one for the decimal point +enum digitEnableStates {DIGIT_ENABLED, DIGIT_DISABLED}; + +byte digitSelectorPin[noOfDigitLocations] = {16,10,13,11}; +byte segmentDrivePin[noOfSegmentsPerDigit] = {2,5,12,6,7,9,8,14}; + +// The final column of segMap[] is for the decimal point status. In this version, +// the decimal point is treated just like all the other segments, so there is +// no need to access this column specifically. +// +byte segMap[noOfPossibleCharacters][noOfSegmentsPerDigit] = { + ON , ON , ON , ON , ON , ON , OFF, OFF, // '0' <- element 0 + OFF, ON , ON , OFF, OFF, OFF, OFF, OFF, // '1' <- element 1 + ON , ON , OFF, ON , ON , OFF, ON , OFF, // '2' <- element 2 + ON , ON , ON , ON , OFF, OFF, ON , OFF, // '3' <- element 3 + OFF, ON , ON , OFF, OFF, ON , ON , OFF, // '4' <- element 4 + ON , OFF, ON , ON , OFF, ON , ON , OFF, // '5' <- element 5 + ON , OFF, ON , ON , ON , ON , ON , OFF, // '6' <- element 6 + ON , ON , ON , OFF, OFF, OFF, OFF, OFF, // '7' <- element 7 + ON , ON , ON , ON , ON , ON , ON , OFF, // '8' <- element 8 + ON , ON , ON , ON , OFF, ON , ON , OFF, // '9' <- element 9 + ON , ON , ON , ON , ON , ON , OFF, ON , // '0.' <- element 10 + OFF, ON , ON , OFF, OFF, OFF, OFF, ON , // '1.' <- element 11 + ON , ON , OFF, ON , ON , OFF, ON , ON , // '2.' <- element 12 + ON , ON , ON , ON , OFF, OFF, ON , ON , // '3.' <- element 13 + OFF, ON , ON , OFF, OFF, ON , ON , ON , // '4.' <- element 14 + ON , OFF, ON , ON , OFF, ON , ON , ON , // '5.' <- element 15 + ON , OFF, ON , ON , ON , ON , ON , ON , // '6.' <- element 16 + ON , ON , ON , OFF, OFF, OFF, OFF, ON , // '7.' <- element 17 + ON , ON , ON , ON , ON , ON , ON , ON , // '8.' <- element 18 + ON , ON , ON , ON , OFF, ON , ON , ON , // '9.' <- element 19 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, // ' ' <- element 20 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, ON // '.' <- element 11 +}; +#endif // PIN_SAVING_HARDWARE + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // energy divertion detection +long surplusPVperMainsCycle_inIEU; + + +void setup() +{ + pinMode(outputForTrigger, OUTPUT); + digitalWrite (outputForTrigger, TRIAC_OFF); // the external trigger is active low + + pinMode(outputModeSelectorPin, INPUT); + digitalWrite(outputModeSelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(outputModeSelectorPin); // initial selection and + outputMode = (enum outputModes)pinState; // assignment of output mode + + delay(5000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_bothDisplays_1.ino"); + Serial.println(); + +#ifdef PIN_SAVING_HARDWARE + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // control lines for the 74HC4543 7-seg display driver and the DP line + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + // control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } +#else + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + pinMode(segmentDrivePin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + pinMode(digitSelectorPin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + digitalWrite(digitSelectorPin[i], DIGIT_DISABLED); } + + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + digitalWrite(segmentDrivePin[i], OFF); } +#endif + + + + // When using integer maths, calibration values that have supplied in floating point + // form need to be rescaled. + // +// phaseCal_grid_int = phaseCal_grid * 256; // for integer maths +// phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)SWEETZONE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + energyInBucket_long = capacityOfEnergyBucket_long * 0.45; // for rapid start up + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + surplusPVperMainsCycle_inIEU = (long)SURPLUS_PV_IN_WATTS * (1/powerCal_grid); + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + configureParamsForSelectedOutputMode(); + + Serial.println ("----"); + +#ifdef WORKLOAD_CHECK + Serial.println ("WELCOME TO WORKLOAD_CHECK "); + +// <<- start of commented out section, to save on RAM space! +/* + Serial.println (" This mode of operation allows the spare processing capacity of the system"); + Serial.println ("to be analysed. Additional delay is gradually increased until all spare time"); + Serial.println ("has been used up. This value (in uS) is noted and the process is repeated. "); + Serial.println ("The delay setting is increased by 1uS at a time, and each value of delay is "); + Serial.println ("checked several times before the delay is increased. "); + */ +// <<- end of commented out section, to save on RAM space! + + Serial.println (" The displayed value is the amount of spare time, per set of V & I samples, "); + Serial.println ("that is available for doing additional processing."); + Serial.println (); + #endif +} + +// An Interrupt Service Routine is now defined in which the ADC is instructed to +// measure V and I alternately. A "data ready"flag is set after each voltage conversion +// has been completed. +// For each pair of samples, this means that current is measured before voltage. The +// current sample is taken first because the phase of the waveform for current is generally +// slightly advanced relative to the waveform for voltage. The data ready flag is cleared +// within loop(). +// This Interrupt Service Routine is for use when the ADC is fixed timer mode. It is +// executed whenever the ADC timer expires. In this mode, the next ADC conversion is +// initiated from within this ISR. +// +void timerIsr(void) +{ + static unsigned char sample_index = 0; + + switch(sample_index) + { + case 0: + sampleV = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current + ADCSRA |= (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples. The main processor and +// the ADC work autonomously, their operation being only linked via the dataReady flag. +// As soon as a new set of data is made available by the ADC, the main processor can +// start to work on it immediately. +// +void allGeneralProcessing() +{ + static boolean triggerNeedsToBeArmed = false; // once per mains cycle (+ve half) + static int samplesDuringThisCycle; // for normalising the power in each mains cycle + static long sumP_grid; // for per-cycle summation of 'real power' + static long sumP_diverted; // for per-cycle summation of 'real power' + static enum polarities polarityOfLastSampleV; // for zero-crossing detection + static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) + static long lastSampleVminusDC_long; // for the phaseCal algorithm + static byte timerForDisplayUpdate = 0; + static enum triacStates nextStateOfTriac = TRIAC_OFF; + + // remove DC offset from the raw voltage sample by subtracting the accurate value + // as determined by a LP filter. + long sampleVminusDC_long = ((long)sampleV<<8) - DCoffset_V_long; + + // determine the polarity of the latest voltage sample + enum polarities polarityNow; + if(sampleVminusDC_long > 0) { + polarityNow = POSITIVE; } + else { + polarityNow = NEGATIVE; } + + if (polarityNow == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + cycleCount++; + + triggerNeedsToBeArmed = true; // the trigger is armed once during each +ve half-cycle + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / samplesDuringThisCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / samplesDuringThisCycle; // proportional to Watts + + realPower_grid += surplusPVperMainsCycle_inIEU; // <- for balance test + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. + + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) + { + realEnergy_diverted = 0; + } + + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + if(timerForDisplayUpdate > UPDATE_PERIOD_FOR_DISPLAYED_DATA) + { // the 4-digit display needs to be refreshed every few mS. For convenience, + // this action is performed every N times around this processing loop. + timerForDisplayUpdate = 0; + +/* +// Need to comment this section out if WORKLOAD_CHECK is enabled + Serial.print("Diverted: " ); + Serial.print(divertedEnergyTotal_Wh); + Serial.print(" Wh plus "); + Serial.print((powerCal_diverted / CYCLES_PER_SECOND) * divertedEnergyRecent_IEU); + + Serial.print(" J , EDD is" ); +*/ + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + +/* +// Need to comment this section out if WORKLOAD_CHECK is enabled + if (EDD_isActive) { + Serial.println(" on" ); } + else { + Serial.println(" off" ); } +*/ + configureValueForDisplay(); + } + else + { + timerForDisplayUpdate++; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + samplesDuringThisCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + if (triggerNeedsToBeArmed == true) + { + // check to see whether the trigger device can now be reliably armed + if (samplesDuringThisCycle == 3) // much easier than checking the voltage level + { + if (energyInBucket_long < lowerEnergyThreshold_long) { + // when below the lower threshold, always set the triac to "off" + nextStateOfTriac = TRIAC_OFF; } + else + if (energyInBucket_long > upperEnergyThreshold_long) { + // when above the upper threshold, always set the triac to "off" + nextStateOfTriac = TRIAC_ON; } + else { + // otherwise, leave the triac's state unchanged (hysteresis) + } + + // set the Arduino's output pin accordingly, and clear the flag + digitalWrite(outputForTrigger, nextStateOfTriac); + triggerNeedsToBeArmed = false; + + // update the Energy Diversion Detector + if (nextStateOfTriac == TRIAC_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + } + } + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > startUpPeriod * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + samplesDuringThisCycle = 0; + Serial.println ("Go!"); + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + checkOutputModeSelection(); // updates outputMode if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is positive + + // processing for EVERY pair of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform +// long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform +// long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + samplesDuringThisCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityOfLastSampleV = polarityNow; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + + +// this function changes the value of outputMode if the state of the external switch is altered +void checkOutputModeSelection() +{ + static byte count = 0; + int pinState = digitalRead(outputModeSelectorPin); + if (pinState != outputMode) + { + count++; + } + if (count >= 20) + { + count = 0; + outputMode = (enum outputModes)pinState; // change the global variable + Serial.print ("outputMode selection changed to "); + if (outputMode == NORMAL) { + Serial.println ( "normal"); } + else { + Serial.println ( "anti-flicker"); } + + configureParamsForSelectedOutputMode(); + } +} + + +void configureParamsForSelectedOutputMode() +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperEnergyThreshold_long = + capacityOfEnergyBucket_long * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + upperEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_long = "); + Serial.println(capacityOfEnergyBucket_long); + Serial.print(" lowerEnergyThreshold_long = "); + Serial.println(lowerEnergyThreshold_long); + Serial.print(" upperEnergyThreshold_long = "); + Serial.println(upperEnergyThreshold_long); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + +// Serial.println(divertedEnergyTotal_Wh); + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +// valueToBeDisplayed++; +} + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + // The two versions of the hardware require different logic. + +#ifdef PIN_SAVING_HARDWARE + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } + +#else // PIN_SAVING_HARDWARE + + // This version is more straightforward because the digit-enable lines can be + // used to mask out all of the transitory states, including the Decimal Point. + // The sequence is: + // + // 1. de-activate the digit-enable line that was previously active + // 2. determine the next location which is to be active + // 3. determine the relevant character for the new active location + // 4. set up the segment drivers for the character to be displayed (includes the DP) + // 5. activate the digit-enable line for the new active location + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + displayTime_count = 0; + + // 1. de-activate the location which is currently being displayed + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_DISABLED); + + // 2. determine the next digit location which is to be displayed + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 3. determine the relevant character for the new active location + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 4. set up the segment drivers for the character to be displayed (includes the DP) + for (byte segment = 0; segment < noOfSegmentsPerDigit; segment++) { + byte segmentState = segMap[digitVal][segment]; + digitalWrite( segmentDrivePin[segment], segmentState); } + + // 5. activate the digit-enable line for the new active location + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_ENABLED); + } +#endif // PIN_SAVING_HARDWARE + +} // end of refreshDisplay() + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/blink_dig4.ino b/docs/routers/mk2pvrouter.co.uk/blink_dig4.ino new file mode 100644 index 0000000..acda3d0 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/blink_dig4.ino @@ -0,0 +1,37 @@ + +/* A modified version of the standard example sketch, blink.ino, in which + * digital pin 4 is driven up and down instead of pin 13. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + * February 2014 + */ + + +/* + Blink + Turns on an LED on for one second, then off for one second, repeatedly. + + This example code is in the public domain. + */ + +// Pin 4 is used by my Mk2 PV Router PCB, rev 1.1, to control the output device +int controlPin = 4; + +// The signal from pin 4 is generally used in an active low manner +#define ON LOW +#define OFF HIGH + +// the setup routine runs once when you press reset: +void setup() { + // initialize the digital pin as an output. + pinMode(controlPin, OUTPUT); +} + +// the loop routine runs over and over again forever: +void loop() { + digitalWrite(controlPin, ON); // drives the output pin low + delay(1000); // wait for a second + digitalWrite(controlPin, OFF); // drives the output pin high + delay(2000); // wait for a second +} diff --git a/docs/routers/mk2pvrouter.co.uk/cal_CT1_v_meter.ino b/docs/routers/mk2pvrouter.co.uk/cal_CT1_v_meter.ino new file mode 100644 index 0000000..b605ac8 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/cal_CT1_v_meter.ino @@ -0,0 +1,496 @@ +/* cal_CT1_v_meter.ino + * + * February 2018 + * This calibration sketch is based on Mk2_bothDisplays_4.ino. Its purpose is to + * mimic the behaviour of a digital electricity meter. + * + * CT1 should be clipped around one of the live cables that pass through the + * meter. The energy flow measured by CT1 is noted and a short pulse is generated + * whenever a pre-set amount of energy has been recorded (normally 3600J). + * + * This stream of pulses can then be compared against optical pulses from a standard + * electrical utility meter. The pulse rate can be varied by adjusting the value + * of powerCal_grid. When the two streams of pulses are in synch, correct calibration + * of the CT1 channel has been achieved. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Change this value to suit the local mains frequency +#define CYCLES_PER_SECOND 50 + +// Change this value to suit the electricity meter's Joules-per-flash rate. +#define ENERGY_BUCKET_CAPACITY_IN_JOULES 3600 + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum LED_states {LED_ON, LED_OFF}; // active low for use at the "trigger" port which is active low + +// allocation of digital pins +// ************************** +const byte outputForLED = 4; // <-- the "trigger" port is active-low + +// allocation of analogue pins +// *************************** +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + +const byte delayBeforeSerialStarts = 1; // in seconds, to allow Serial window to be opened +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +long cycleCount = 0; // used to time LED events, rather than calling millis() +int samplesDuringThisMainsCycle = 0; + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +int phaseCal_grid_int; // to avoid the need for floating-point maths +int phaseCal_diverted_int; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +volatile int sampleI_grid; +volatile int sampleI_diverted; +volatile int sampleV; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define PERSISTENCE_FOR_POLARITY_CHANGE 1 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + +// for control of the LED at the "trigger" port (port D4) +enum LED_states LED_state; +boolean LED_pulseInProgress = false; +unsigned long LED_onAt; + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms as recorded by the processor. This provides +// a simple way for the user to be confident that their system has been set up +// correctly for the power levels that are to be measured. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt. +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This value is numerically smaller than the +// likely output signal from the ADC when measuring current by a factor of +// approximately twenty. The conversion rate of the overall system for measuring +// CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0446; // for CT1 +const float powerCal_diverted = 0.05; // for CT2 <-- not used in this sketch + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When +// measuring a resistive load, the voltage and current waveforms should be perfectly +// aligned. In this situation, the calculated Power Factor will be 1. +// +const float phaseCal_grid = 1.0; +const float phaseCal_diverted = 1.0; + + +void setup() +{ + pinMode(outputForLED, OUTPUT); + delay (100); + LED_state = LED_ON; // to mimic the behaviour of an electricity + digitalWrite(outputForLED, LED_state); // meter which starts up in 'sleep' mode + + delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: cal_CT1_v_meter.ino"); + Serial.println(); + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)ENERGY_BUCKET_CAPACITY_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + energyInBucket_long = 0; + + // When using integer maths, calibration values that have supplied in floating point + // form need to be rescaled. + // + phaseCal_grid_int = phaseCal_grid * 256; // for integer maths + phaseCal_diverted_int = phaseCal_diverted * 256; // for integer maths + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1< 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + cycleCount++; + if (beyondStartUpPhase) + { + // a simple routine for checking the performance of this new ISR structure + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. Most meters generate a visible pulse + // when a certain amount of forward energy flow has been recorded, often 3600 Joules. + // For this calibration sketch, the capacity of the energy bucket is set to this same + // value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // when operating as a cal program + if (energyInBucket_long > capacityOfEnergyBucket_long) + { + energyInBucket_long -= capacityOfEnergyBucket_long; + registerConsumedPower(); + } + + if (energyInBucket_long < 0) + { + digitalWrite(outputForLED, LED_ON); // to mimic the nehaviour of an electricity meter + energyInBucket_long = 0; + } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_grid = 0; + check_LED_status(); + + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sampleSetsDuringThisMainsCycle = 0; // not yet dealt with for this cycle + sampleCount_forContinuityChecker = 1; // opportunity has been missed for this cycle + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12); + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is negative + + // processing for EVERY set of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform + long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); +// long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + sampleSetsDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries +} +// ----- end of main Mk2i code ----- + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count > PERSISTENCE_FOR_POLARITY_CHANGE) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + + + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + +void registerConsumedPower() +{ + LED_onAt = cycleCount; + LED_state = LED_ON; + digitalWrite(outputForLED, LED_state); + LED_pulseInProgress = true; +} + +void check_LED_status() +{ + if (LED_pulseInProgress == true) + { + if (cycleCount > (LED_onAt + 2)) // normal pulse duration + { + LED_state = LED_OFF; + digitalWrite(outputForLED, LED_state); + LED_pulseInProgress = false; + } + } +} + + + diff --git a/docs/routers/mk2pvrouter.co.uk/cal_CT2_v_CT1.ino b/docs/routers/mk2pvrouter.co.uk/cal_CT2_v_CT1.ino new file mode 100644 index 0000000..dade8b8 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/cal_CT2_v_CT1.ino @@ -0,0 +1,591 @@ +/* cal_CT2_v_CT1.ino + * + * February 2018 + * This calibration sketch is based on Mk2_bothDisplays_4.ino. Its purpose is to + * mimic the behaviour of a dual channel electricity meter. The sensitivity of the + * CT2 channel can then be adjusted to match that of the CT1 channel. Before using + * this sketch, the CT1 channel would normally have been calibrated against the + * user's electricity meter. + * + * CT1 and CT2 should be fitted around the same current-carrying conductor. If + * CT2 has been built into a completed system, the bypass switch can be used to force + * power down that path. + * + * The energy flow on each channel is noted and a short pulse is generated whenever a + * pre-set amount of energy has been recorded (normally 3600J). The two streams of + * pulses can then be compared. The pulse rate for the CT2 channel can be varied by + * adjusting the value of powerCal_diverted. When the two streams of pulses are in + * synch, correct calibration of the CT2 channel has been achieved. + * + * The two pulse streams can be synchronised at any time by earthing R11 which is + * tracked to port A0 (aka D14). + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Change this value to suit the local mains frequency +#define CYCLES_PER_SECOND 50 + +// Change this value to suit the electricity meter's Joules-per-flash rate. +#define ENERGY_BUCKET_CAPACITY_IN_JOULES 3600 + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum LEDforCT1_states {LED_ON_FOR_CT1, LED_OFF_FOR_CT1}; // active low for use at the "trigger" port +enum LEDforCT2_states {LED_OFF_FOR_CT2, LED_ON_FOR_CT2}; // active high for use at the "mode" port +enum resetLineStates {ACTIVE, INACTIVE}; +//enum resetLineStates resetLineState; + +// allocation of digital pins +// ************************** +const byte LEDforCT1 = 4; // <-- the "trigger" port is active-low +const byte LEDforCT2 = 3; // <-- the "mode" port is active-high + +// allocation of analogue pins +// *************************** +const byte resetLine = 14; // (port A0 can also be addressed as D14) +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_forCT2 = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_forCT1 = 5; // A5 is for CT1 which measures grid current + +const byte delayBeforeSerialStarts = 1; // in seconds, to allow Serial window to be opened +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +long cycleCount = 0; // used to time LED events, rather than calling millis() +int samplesDuringThisMainsCycle = 0; + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long energyInBucket_long_forCT1; // in Integer Energy Units +long energyInBucket_long_forCT2; // in Integer Energy Units +long capacityOfEnergyBucket_long_forCT1; // depends on powerCal & frequency +long capacityOfEnergyBucket_long_forCT2; // depends on powerCal * frequency +int phaseCal_int_forCT1; // to avoid the need for floating-point maths +int phaseCal_int_forCT2; // to avoid the need for floating-point maths +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +volatile int sampleI_forCT1; +volatile int sampleI_forCT2; +volatile int sampleV; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define PERSISTENCE_FOR_POLARITY_CHANGE 1 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + +// The LED for the CT1 channel is controlled by the active-low "trigger" port (port D4) +enum LEDforCT1_states LEDstate_forCT1; +boolean LEDforCT1_pulseInProgress = false; +unsigned long LEDforCT1_onAt; + +// The LED for the CT2 channel is controlled by the active-high "mode" port (port D3) +enum LEDforCT2_states LEDstate_forCT2; +boolean LEDforCT2_pulseInProgress = false; +unsigned long LEDforCT2_onAt; + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms as recorded by the processor. This provides +// a simple way for the user to be confident that their system has been set up +// correctly for the power levels that are to be measured. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt. +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This value is numerically smaller than the +// likely output signal from the ADC when measuring current by a factor of +// approximately twenty. The conversion rate of the overall system for measuring +// CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_forCT1 = 0.05; // for CT1 +const float powerCal_forCT2 = 0.0487; // for CT2 + + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// Values ouside the 0 to 1 range involve extrapolation, rather than interpolation +// and are not recommended. By altering the order in which V and I samples are +// taken, and for how many loops they are stored, it should always be possible to +// arrange for the optimal value of phaseCal to lie within the range 0 to 1. When +// measuring a resistive load, the voltage and current waveforms should be perfectly +// aligned. In this situation, the calculated Power Factor will be 1. +// +const float phaseCal_forCT1 = 1.0; +const float phaseCal_forCT2 = 1.0; + + +void setup() +{ + LEDstate_forCT1 = LED_ON_FOR_CT1; // + LEDstate_forCT2 = LED_ON_FOR_CT2; // to mimic the behaviour of an electricity + digitalWrite(LEDforCT1, LEDstate_forCT1); // meter which starts up in 'sleep' mode + digitalWrite(LEDforCT2, LEDstate_forCT2); // + pinMode(LEDforCT1, OUTPUT); + pinMode(LEDforCT2, OUTPUT); + + pinMode(resetLine, INPUT); + digitalWrite(resetLine, HIGH); // enable the internal pullup resistor + + delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: cal_CT1_against_CT2.ino"); + Serial.println(); + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // For the flow of energy at CT1 + capacityOfEnergyBucket_long_forCT1 = + (long)ENERGY_BUCKET_CAPACITY_IN_JOULES * CYCLES_PER_SECOND * (1 / powerCal_forCT1); + energyInBucket_long_forCT1 = 0; + // + // For the flow of energy at CT2 + capacityOfEnergyBucket_long_forCT2 = + (long)ENERGY_BUCKET_CAPACITY_IN_JOULES * CYCLES_PER_SECOND * (1 / powerCal_forCT2); + energyInBucket_long_forCT2 = 0; + + // When using integer maths, calibration values that have supplied in floating point + // form need to be rescaled. + // + phaseCal_int_forCT1 = phaseCal_forCT1 * 256; // for integer maths + phaseCal_int_forCT2 = phaseCal_forCT2 * 256; // for integer maths + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1 << ADPS0) + (1 << ADPS1) + (1 << ADPS2); // Set the ADC's clock to system clock / 128 + ADCSRA |= (1 << ADEN); // Enable ADC + + Timer1.initialize(ADC_TIMER_PERIOD); // set Timer1 interval + Timer1.attachInterrupt( timerIsr ); // declare timerIsr() as interrupt service routine + + Serial.print ( "powerCal_forCT1 = "); Serial.println (powerCal_forCT1, 4); + Serial.print ( "powerCal_forCT2 = "); Serial.println (powerCal_forCT2, 4); + + Serial.print ("zero-crossing persistence (sample sets) = "); + Serial.println (PERSISTENCE_FOR_POLARITY_CHANGE); + Serial.print ("continuity sampling display rate (mains cycles) = "); + Serial.println (CONTINUITY_CHECK_MAXCOUNT); + + Serial.println ("----"); +} + +// An Interrupt Service Routine is now defined in which the ADC is instructed to +// measure each analogue input in sequence. A "data ready"flag is set after each +// voltage conversion has been completed. +// For each set of samples, the two samples for current are taken before the one +// for voltage. This is appropriate because each waveform current is generally slightly +// advanced relative to the waveform for voltage. The data ready flag is cleared +// within loop(). +// This Interrupt Service Routine is for use when the ADC is fixed timer mode. It is +// executed whenever the ADC timer expires. In this mode, the next ADC conversion is +// initiated from within this ISR. +// +void timerIsr(void) +{ + static unsigned char sample_index = 0; + static int sampleI_forCT1_raw; + static int sampleI_forCT2_raw; + + + switch (sample_index) + { + case 0: + sampleV = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_forCT2; // set up the next conversion, which is for Diverted Current + ADCSRA |= (1 << ADSC); // start the ADC + sample_index++; // increment the control flag + sampleI_forCT2 = sampleI_forCT2_raw; + sampleI_forCT1 = sampleI_forCT1_raw; + dataReady = true; // all three ADC values can now be processed + break; + case 1: + sampleI_forCT2_raw = ADC; // store the ADC value (this one is for Diverted Current) + ADMUX = 0x40 + currentSensor_forCT1; // set up the next conversion, which is for Grid Current + ADCSRA |= (1 << ADSC); // start the ADC + sample_index++; // increment the control flag + break; + case 2: + sampleI_forCT1_raw = ADC; // store the ADC value (this one is for Grid Current) + ADMUX = 0x40 + voltageSensor; // set up the next conversion, which is for Voltage + ADCSRA |= (1 << ADSC); // start the ADC + sample_index = 0; // reset the control flag + break; + default: + sample_index = 0; // to prevent lockup (should never get here) + } +} + + +// When using interrupt-based logic, the main processor waits in loop() until the +// dataReady flag has been set by the ADC. Once this flag has been set, the main +// processor clears the flag and proceeds with all the processing for one set of +// V & I samples. It then returns to loop() to wait for the next set to become +// available. +// +void loop() +{ + if (dataReady) // flag is set after every set of ADC conversions + { + dataReady = false; // reset the flag + allGeneralProcessing(); // executed once for each set of V&I samples + } +} // end of loop() + + +// This routine is called to process each set of V & I samples. The main processor and +// the ADC work autonomously, their operation being only linked via the dataReady flag. +// As soon as a new set of data is made available by the ADC, the main processor can +// start to work on it immediately. +// +void allGeneralProcessing() +{ + static long sumP_forCT1; // for per-cycle summation of 'real power' at CT1 + static long sumP_forCT2; // for per-cycle summation of 'real power' at CT2 + static long cumVdeltasThisCycle_long; // for the LPF which determines DC offset (voltage) + static long lastSampleVminusDC_long; // for the phaseCal algorithm + static byte timerForDisplayUpdate = 0; + + // remove DC offset from the raw voltage sample by subtracting the accurate value + // as determined by a LP filter. + long sampleVminusDC_long = ((long)sampleV << 8) - DCoffset_V_long; + + // determine the polarity of the latest voltage sample + if (sampleVminusDC_long > 0) { + polarityOfMostRecentVsample = POSITIVE; + } + else { + polarityOfMostRecentVsample = NEGATIVE; + } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + cycleCount++; + if (beyondStartUpPhase) + { + // a simple routine for checking the performance of this new ISR structure + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; + } + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_forCT1 = sumP_forCT1 / sampleSetsDuringThisMainsCycle; // proportional to Watts + long realPower_forCT2 = sumP_forCT2 / sampleSetsDuringThisMainsCycle; // proportional to Watts + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_forCT1 = realPower_forCT1; + long realEnergy_forCT2 = realPower_forCT2; + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. Most meters generate a visible pulse + // when a certain amount of forward energy flow has been recorded, often 3600 Joules. + // For this calibration sketch, the capacity of the energy bucket is set to this same + // value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long_forCT1 += realEnergy_forCT1; + energyInBucket_long_forCT2 += realEnergy_forCT2; + + // when operating as a cal program + if (energyInBucket_long_forCT1 > capacityOfEnergyBucket_long_forCT1){ + energyInBucket_long_forCT1 -= capacityOfEnergyBucket_long_forCT1; + registerConsumedPower_forCT1(); } + // + if (energyInBucket_long_forCT1 < 0){ + digitalWrite(LEDforCT1, LED_ON_FOR_CT1); // to mimic the nehaviour of an electricity meter + energyInBucket_long_forCT1 = 0; } + // + // + if (energyInBucket_long_forCT2 > capacityOfEnergyBucket_long_forCT2){ + energyInBucket_long_forCT2 -= capacityOfEnergyBucket_long_forCT2; + registerConsumedPower_forCT2(); } + // + if (energyInBucket_long_forCT2 < 0){ + digitalWrite(LEDforCT2, LED_ON_FOR_CT2); // to mimic the nehaviour of an electricity meter + energyInBucket_long_forCT2 = 0; } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_forCT1 = 0; + sumP_forCT2 = 0; + check_LED_status_forCT1(); + check_LED_status_forCT2(); + } + else + { + // wait until the DC-blocking filters have had time to settle + if (millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000) + { + beyondStartUpPhase = true; + sumP_forCT1 = 0; + sumP_forCT2 = 0; + sampleSetsDuringThisMainsCycle = 0; // not yet dealt with for this cycle + sampleCount_forContinuityChecker = 1; // opportunity has been missed for this cycle + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long >> 12); + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; + } + else if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; + } + + checkForReset(); + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is negative + + // processing for EVERY set of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_forCT1 = ((long)(sampleI_forCT1 - DCoffset_I)) << 8; + // + // phase-shift the voltage waveform so that it aligns with the grid current waveform + long phaseShiftedSampleVminusDC_forCT1 = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long) * phaseCal_int_forCT1) >> 8); + // long phaseShiftedSampleVminusDC_forCT1 = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + // + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_forCT1 >> 2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_forCT1 >> 2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP >> 12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_forCT1 += instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Next, deal with the power at the diverted connection point (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_forCT2 = ((long)(sampleI_forCT2 - DCoffset_I)) << 8; + // + // phase-shift the voltage waveform so that it aligns with the grid current waveform + long phaseShiftedSampleVminusDC_forCT2 = lastSampleVminusDC_long + + (((sampleVminusDC_long - lastSampleVminusDC_long) * phaseCal_int_forCT2) >> 8); + // long phaseShiftedSampleVminusDC_forCT2 = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + // + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_forCT2 >> 2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_forCT2 >> 2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP >> 12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_forCT2 += instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + sampleSetsDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries +} + + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + a certain number of consecutive samples in the 'other' half of the + waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; + } + else { + count = 0; + } + + if (count > PERSISTENCE_FOR_POLARITY_CHANGE) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + +void checkForReset() +{ + int pinState = digitalRead(resetLine); + if (pinState == ACTIVE) + { + // Serial.println ("manual reset"); + registerConsumedPower_forCT1(); + energyInBucket_long_forCT1 = capacityOfEnergyBucket_long_forCT1 / 2; + registerConsumedPower_forCT2(); + energyInBucket_long_forCT2 = capacityOfEnergyBucket_long_forCT2 / 2; + } +} + +void registerConsumedPower_forCT1() +{ + LEDforCT1_onAt = cycleCount; + LEDstate_forCT1 = LED_ON_FOR_CT1; + digitalWrite(LEDforCT1, LEDstate_forCT1); + LEDforCT1_pulseInProgress = true; +} + +void registerConsumedPower_forCT2() +{ + LEDforCT2_onAt = cycleCount; + LEDstate_forCT2 = LED_ON_FOR_CT2; + digitalWrite(LEDforCT2, LEDstate_forCT2); + LEDforCT2_pulseInProgress = true; +} + +void check_LED_status_forCT1() +{ + if (LEDforCT1_pulseInProgress == true) + { + if (cycleCount > (LEDforCT1_onAt + 2)) // normal pulse duration + { + LEDstate_forCT1 = LED_OFF_FOR_CT1; + digitalWrite(LEDforCT1, LEDstate_forCT1); + LEDforCT1_pulseInProgress = false; + } + } +} + +void check_LED_status_forCT2() +{ + if (LEDforCT2_pulseInProgress == true) + { + if (cycleCount > (LEDforCT2_onAt + 2)) // normal pulse duration + { + LEDstate_forCT2 = LED_OFF_FOR_CT2; + digitalWrite(LEDforCT2, LEDstate_forCT2); + LEDforCT2_pulseInProgress = false; + } + } +} + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + diff --git a/docs/routers/mk2pvrouter.co.uk/cal_bothDisplays_1.ino b/docs/routers/mk2pvrouter.co.uk/cal_bothDisplays_1.ino new file mode 100644 index 0000000..ddcb097 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/cal_bothDisplays_1.ino @@ -0,0 +1,781 @@ +/* cal_bothDisplays_1.ino + * + * This sketch provides an easy way of calibrating the current sensors that are + * connected to the the CT1 and CT2 ports. Channel selection is provided by a + * switch at the "mode" connector on the main board. The measured value is shown + * on the 4-digit display, and also at the Serial Monitor. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * The selected CT should be clipped around a lead through which a known + * amount of power is flowing. This can be compared against the measured value + * which is proportional to the powerCal setting. Once the optimal powerCal values + * have been obtained for each channel, these values can be transferred into the + * main Mk2 PV Router sketch. + * + * Depending on which way around the CT is connected, the measured value may be + * positive or negative. If it is negative, the display will either flash or + * display a negative symbol. Its behaviour will depend on the way that the display + * has been configured. + * + * The 4-digit display can be driven in two different ways, one with an extra pair + * of logic chips, and one without. The appropriate version of the sketch must be + * selected by including or commenting out the "#define PIN_SAVING_HARDWARE" + * statement near the top of the code. + * + * With the pin-saving logic, the display is not able to show a '-' symbol. But + * in the alternative mode, it is. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + * March 2014 + */ + +#include + +#include +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +// The two versions of the hardware require different logic. The following line should +// be included if the additional logic chips are present, or excluded if they are +// absent (in which case some wire links need to be fitted) +// +//#define PIN_SAVING_HARDWARE + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum triacStates {TRIAC_ON, TRIAC_OFF}; // the external trigger device is active low +enum powerChannels {DIVERTED, GRID}; + +// The selected channel is determined in realtime via an external switch +enum powerChannels powerChannel; + +// allocation of digital pins for prototype PCB-based rig (with simple display adapter) +// ****************************************************** +// D0 & D1 are reserved for the Serial i/f +// D2 is a driver line for the 4-digit display (segment D, via series resistor) +const byte powerChannelSelectorPin = 3; // <-- with the internal pullup +// D4 is not used +// D5 is a driver line for the 4-digit display (segment B, via series resistor) +// D6 is a driver line for the 4-digit display (digit 3, via wire link) +// D7 is a driver line for the 4-digit display (digit 2, via wire link) +// D8 is a driver line for the 4-digit display (segment F, via series resistor) +// D9 is a driver line for the 4-digit display (segment A, via series resistor) +// D10 is a driver line for the 4-digit display (segment DP, via series resistor) +// D11 is a driver line for the 4-digit display (segment C, via series resistor) +// D12 is a driver line for the 4-digit display (segment G, via series resistor) +// D13 is a driver line for the 4-digit display (digit 4, via wire link) + +// allocation of analogue pins +// *************************** +// A0 (D14) is a driver line for the 4-digit display (digit 1, via wire link) +// A1 (D15) is a driver line for the 4-digit display (segment E, via series resistor) +// A2 (D16) is not used +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + + +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long cycleCount = 0; // counts mains cycles from start-up +long energyThisSecond_grid; +long energyThisSecond_diverted; +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long IEU_per_Joule_grid; +long IEU_per_Joule_diverted; + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +int sampleI_grid; +int sampleI_diverted; +int sampleV; + + +// Calibration +//------------ +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// With my PCB-based hardware, the ADC has an input range of 0 to 3.3V and an +// output range of 0 to 1023. The purpose of each input sensor is to +// convert the measured parameter into a low-voltage signal which fits nicely +// within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal of +// the ADC by around a factor of twenty. The conversion rate of the overall system +// for measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.05; // for CT1 +const float powerCal_diverted = 0.05; // for CT2 + + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 23; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates + +// The two versions of the hardware require different logic. +#ifdef PIN_SAVING_HARDWARE + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. In this version, +// the decimal point has to be treated differently than the other seven segments, so +// a convenient means of accessing this column is provided. +// +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH, // '.' <- element 21 + HIGH, HIGH, HIGH, HIGH, LOW // ' ' <- element 22 (for compatibility) +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + +#else // PIN_SAVING_HARDWARE + +#define ON HIGH +#define OFF LOW + +const byte noOfSegmentsPerDigit = 8; // includes one for the decimal point +enum digitEnableStates {DIGIT_ENABLED, DIGIT_DISABLED}; + +byte digitSelectorPin[noOfDigitLocations] = {16,10,13,11}; +byte segmentDrivePin[noOfSegmentsPerDigit] = {2,5,12,6,7,9,8,14}; + +// The final column of segMap[] is for the decimal point status. In this version, +// the decimal point is treated just like all the other segments, so there is +// no need to access this column specifically. +// +byte segMap[noOfPossibleCharacters][noOfSegmentsPerDigit] = { + ON , ON , ON , ON , ON , ON , OFF, OFF, // '0' <- element 0 + OFF, ON , ON , OFF, OFF, OFF, OFF, OFF, // '1' <- element 1 + ON , ON , OFF, ON , ON , OFF, ON , OFF, // '2' <- element 2 + ON , ON , ON , ON , OFF, OFF, ON , OFF, // '3' <- element 3 + OFF, ON , ON , OFF, OFF, ON , ON , OFF, // '4' <- element 4 + ON , OFF, ON , ON , OFF, ON , ON , OFF, // '5' <- element 5 + ON , OFF, ON , ON , ON , ON , ON , OFF, // '6' <- element 6 + ON , ON , ON , OFF, OFF, OFF, OFF, OFF, // '7' <- element 7 + ON , ON , ON , ON , ON , ON , ON , OFF, // '8' <- element 8 + ON , ON , ON , ON , OFF, ON , ON , OFF, // '9' <- element 9 + ON , ON , ON , ON , ON , ON , OFF, ON , // '0.' <- element 10 + OFF, ON , ON , OFF, OFF, OFF, OFF, ON , // '1.' <- element 11 + ON , ON , OFF, ON , ON , OFF, ON , ON , // '2.' <- element 12 + ON , ON , ON , ON , OFF, OFF, ON , ON , // '3.' <- element 13 + OFF, ON , ON , OFF, OFF, ON , ON , ON , // '4.' <- element 14 + ON , OFF, ON , ON , OFF, ON , ON , ON , // '5.' <- element 15 + ON , OFF, ON , ON , ON , ON , ON , ON , // '6.' <- element 16 + ON , ON , ON , OFF, OFF, OFF, OFF, ON , // '7.' <- element 17 + ON , ON , ON , ON , ON , ON , ON , ON , // '8.' <- element 18 + ON , ON , ON , ON , OFF, ON , ON , ON , // '9.' <- element 19 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, // ' ' <- element 20 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, ON , // '.' <- element 21 + OFF, OFF, OFF, OFF, OFF, OFF, ON , OFF // '-' <- element 22 +}; +#endif // PIN_SAVING_HARDWARE + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + + +void setup() +{ + delay(5000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: cal_bothDisplays_1.ino"); + Serial.println(); + + pinMode(powerChannelSelectorPin, INPUT); + digitalWrite(powerChannelSelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(powerChannelSelectorPin); // initial selection and + powerChannel = (enum powerChannels)pinState; // assignment of output mode + + Serial.print ("powerChannel is set to "); + if (powerChannel == GRID) { + Serial.println ( "grid (CT1)"); } + else { + Serial.println ( "diverted (CT2)"); } + + #ifdef PIN_SAVING_HARDWARE + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // control lines for the 74HC4543 7-seg display driver and the DP line + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + // control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } +#else + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + pinMode(segmentDrivePin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + pinMode(digitSelectorPin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + digitalWrite(digitSelectorPin[i], DIGIT_DISABLED); } + + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + digitalWrite(segmentDrivePin[i], OFF); } +#endif + + // For converting Integer Energy Units into Joules + IEU_per_Joule_grid = (long)CYCLES_PER_SECOND * (1/powerCal_grid); + IEU_per_Joule_diverted = (long)CYCLES_PER_SECOND * (1/powerCal_diverted); + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + + Serial.println ("----"); +} + +// An Interrupt Service Routine is now defined in which the ADC is instructed to +// perform the three analogue measurements in sequence: Voltage, I_diverted and I_grid. +// A "data ready" flag is set after each voltage conversion has been completed. This +// flag is cleared within loop(). +// This Interrupt Service Routine is executed whenever the ADC timer expires. The next +// ADC conversion is initiated from within this ISR. +// +void timerIsr(void) +{ + static unsigned char sample_index = 0; + + switch(sample_index) + { + case 0: + sampleV = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current + ADCSRA |= (1< 0) { + polarityNow = POSITIVE; } + else { + polarityNow = NEGATIVE; } + + if (polarityNow == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + cycleCount++; + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / samplesDuringThisCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / samplesDuringThisCycle; // proportional to Watts + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling intervals are of equal duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Add these energy contributions to the relevant accumulator. + energyThisSecond_grid += realEnergy_grid; + energyThisSecond_diverted += realEnergy_diverted; + + // Once per second, the contents of the selected accumulator is + // converted to Joules and displayed as Watts. Both accumulators + // are then reset. + // + if(perSecondCounter >= CYCLES_PER_SECOND) + { + perSecondCounter = 0; + int powerInWatts; + + if(powerChannel == GRID) { + powerInWatts = energyThisSecond_grid / IEU_per_Joule_grid; } + else { + powerInWatts = energyThisSecond_diverted / IEU_per_Joule_diverted; } + + configureValueForDisplay(powerInWatts); + energyThisSecond_grid = 0; + energyThisSecond_diverted = 0; + } + else + { + perSecondCounter++; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + samplesDuringThisCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > startUpPeriod * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + samplesDuringThisCycle = 0; + Serial.println ("Go!"); + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + checkPowerChannelSelection(); // updates outputMode if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is negative + + // processing for EVERY pair of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform +// long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform +// long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + samplesDuringThisCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityOfLastSampleV = polarityNow; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + + +// this function changes the value of powerChannel if the state of the external switch is altered +void checkPowerChannelSelection() +{ + static byte count = 0; + int pinState = digitalRead(powerChannelSelectorPin); + if (pinState != powerChannel) + { + count++; + } + if (count >= 20) + { + count = 0; + powerChannel = (enum powerChannels)pinState; // change the global variable + Serial.print ("powerChannel selection changed to "); + if (powerChannel == GRID) { + Serial.println ( "grid (CT1)"); } + else { + Serial.println ( "diverted (CT2)"); } + } +} + + +// called every second, to update the characters to be displayed +void configureValueForDisplay(int power) +{ + boolean energyValueExceeds10kWh; + boolean powerIsNegative; + int val; + static boolean perSecondToggle = false; + + if (power < 0) { + powerIsNegative = true; + val = -1 * power; } + else { + powerIsNegative = false; + val = power; } + + if (powerIsNegative && !perSecondToggle) + { + // do something different + charsForDisplay[0] = 22; + charsForDisplay[1] = 20; + charsForDisplay[2] = 20; + charsForDisplay[3] = 20; + } + else + { + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + + perSecondToggle = !perSecondToggle; + + Serial.println(power); // in case a display is not available +} + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + // The two versions of the hardware require different logic. + +#ifdef PIN_SAVING_HARDWARE + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } + +#else // PIN_SAVING_HARDWARE + + // This version is more straightforward because the digit-enable lines can be + // used to mask out all of the transitory states, including the Decimal Point. + // The sequence is: + // + // 1. de-activate the digit-enable line that was previously active + // 2. determine the next location which is to be active + // 3. determine the relevant character for the new active location + // 4. set up the segment drivers for the character to be displayed (includes the DP) + // 5. activate the digit-enable line for the new active location + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + displayTime_count = 0; + + // 1. de-activate the location which is currently being displayed + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_DISABLED); + + // 2. determine the next digit location which is to be displayed + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 3. determine the relevant character for the new active location + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 4. set up the segment drivers for the character to be displayed (includes the DP) + for (byte segment = 0; segment < noOfSegmentsPerDigit; segment++) { + byte segmentState = segMap[digitVal][segment]; + digitalWrite( segmentDrivePin[segment], segmentState); } + + // 5. activate the digit-enable line for the new active location + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_ENABLED); + } +#endif // PIN_SAVING_HARDWARE + +} // end of refreshDisplay() + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/cal_bothDisplays_2.ino b/docs/routers/mk2pvrouter.co.uk/cal_bothDisplays_2.ino new file mode 100644 index 0000000..9cdbd79 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/cal_bothDisplays_2.ino @@ -0,0 +1,779 @@ +/* cal_bothDisplays_2.ino + * + * This sketch provides an easy way of calibrating the current sensors that are + * connected to the the CT1 and CT2 ports. Channel selection is provided by a + * switch at the "mode" connector on the main board. The measured value is shown + * on the 4-digit display, and also at the Serial Monitor. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * The selected CT should be clipped around a lead through which a known + * amount of power is flowing. This can be compared against the measured value + * which is proportional to the powerCal setting. Once the optimal powerCal values + * have been obtained for each channel, these values can be transferred into the + * main Mk2 PV Router sketch. + * + * Depending on which way around the CT is connected, the measured value may be + * positive or negative. If it is negative, the display will either flash or + * display a negative symbol. Its behaviour will depend on the way that the display + * has been configured. + * + * The 4-digit display can be driven in two different ways, one with an extra pair + * of logic chips, and one without. The appropriate version of the sketch must be + * selected by including or commenting out the "#define PIN_SAVING_HARDWARE" + * statement near the top of the code. + * + * With the pin-saving logic, the display is not able to show a '-' symbol. But + * in the alternative mode, it is. + * + * December 2017, upgraded to cal_bothDisplays_2: + * In the original version, the mains cycle counter cycled through the values 0 to + * CYCLES_PER_SECOND inclusive so data was only processed every (CYCLES_PER_SECOND + 1) + * mains cycles. Because the accumulated energy data was divided by CYCLES_PER_SECOND, + * there was an error of approx 2% in the displayed power value. In version 2, + * the logic has been corrected to avoid this error. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + * December 2017 + */ + +#include + +#include +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +// The two versions of the hardware require different logic. The following line should +// be included if the additional logic chips are present, or excluded if they are +// absent (in which case some wire links need to be fitted) +// +//#define PIN_SAVING_HARDWARE + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum triacStates {TRIAC_ON, TRIAC_OFF}; // the external trigger device is active low +enum powerChannels {DIVERTED, GRID}; + +// The selected channel is determined in realtime via an external switch +enum powerChannels powerChannel; + +// allocation of digital pins (excluding the display which is dealt with separately) +// ****************************************************** +// D0 & D1 are reserved for the Serial i/f +const byte powerChannelSelectorPin = 3; // <-- with the internal pullup + +// allocation of analogue pins (excluding the display which is dealt with separately) +// *************************** +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + + +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long cycleCount = 0; // counts mains cycles from start-up +long energyThisSecond_grid; +long energyThisSecond_diverted; +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long IEU_per_Joule_grid; +long IEU_per_Joule_diverted; + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +int sampleI_grid; +int sampleI_diverted; +int sampleV; + + +// Calibration +//------------ +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// With my PCB-based hardware, the ADC has an input range of 0 to 3.3V and an +// output range of 0 to 1023. The purpose of each input sensor is to +// convert the measured parameter into a low-voltage signal which fits nicely +// within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal of +// the ADC by around a factor of twenty. The conversion rate of the overall system +// for measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.05; // for CT1 +const float powerCal_diverted = 0.05; // for CT2 + + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 23; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates + +// The two versions of the hardware require different logic. +#ifdef PIN_SAVING_HARDWARE + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. In this version, +// the decimal point has to be treated differently than the other seven segments, so +// a convenient means of accessing this column is provided. +// +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH, // '.' <- element 21 + HIGH, HIGH, HIGH, HIGH, LOW // ' ' <- element 22 (for compatibility) +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + +#else // PIN_SAVING_HARDWARE + +#define ON HIGH +#define OFF LOW + +const byte noOfSegmentsPerDigit = 8; // includes one for the decimal point +enum digitEnableStates {DIGIT_ENABLED, DIGIT_DISABLED}; + +byte digitSelectorPin[noOfDigitLocations] = {16,10,13,11}; +byte segmentDrivePin[noOfSegmentsPerDigit] = {2,5,12,6,7,9,8,14}; + +// The final column of segMap[] is for the decimal point status. In this version, +// the decimal point is treated just like all the other segments, so there is +// no need to access this column specifically. +// +byte segMap[noOfPossibleCharacters][noOfSegmentsPerDigit] = { + ON , ON , ON , ON , ON , ON , OFF, OFF, // '0' <- element 0 + OFF, ON , ON , OFF, OFF, OFF, OFF, OFF, // '1' <- element 1 + ON , ON , OFF, ON , ON , OFF, ON , OFF, // '2' <- element 2 + ON , ON , ON , ON , OFF, OFF, ON , OFF, // '3' <- element 3 + OFF, ON , ON , OFF, OFF, ON , ON , OFF, // '4' <- element 4 + ON , OFF, ON , ON , OFF, ON , ON , OFF, // '5' <- element 5 + ON , OFF, ON , ON , ON , ON , ON , OFF, // '6' <- element 6 + ON , ON , ON , OFF, OFF, OFF, OFF, OFF, // '7' <- element 7 + ON , ON , ON , ON , ON , ON , ON , OFF, // '8' <- element 8 + ON , ON , ON , ON , OFF, ON , ON , OFF, // '9' <- element 9 + ON , ON , ON , ON , ON , ON , OFF, ON , // '0.' <- element 10 + OFF, ON , ON , OFF, OFF, OFF, OFF, ON , // '1.' <- element 11 + ON , ON , OFF, ON , ON , OFF, ON , ON , // '2.' <- element 12 + ON , ON , ON , ON , OFF, OFF, ON , ON , // '3.' <- element 13 + OFF, ON , ON , OFF, OFF, ON , ON , ON , // '4.' <- element 14 + ON , OFF, ON , ON , OFF, ON , ON , ON , // '5.' <- element 15 + ON , OFF, ON , ON , ON , ON , ON , ON , // '6.' <- element 16 + ON , ON , ON , OFF, OFF, OFF, OFF, ON , // '7.' <- element 17 + ON , ON , ON , ON , ON , ON , ON , ON , // '8.' <- element 18 + ON , ON , ON , ON , OFF, ON , ON , ON , // '9.' <- element 19 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, // ' ' <- element 20 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, ON , // '.' <- element 21 + OFF, OFF, OFF, OFF, OFF, OFF, ON , OFF // '-' <- element 22 +}; +#endif // PIN_SAVING_HARDWARE + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + + +void setup() +{ + delay(5000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: cal_bothDisplays_2.ino"); + Serial.println(); + + pinMode(powerChannelSelectorPin, INPUT); + digitalWrite(powerChannelSelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(powerChannelSelectorPin); // initial selection and + powerChannel = (enum powerChannels)pinState; // assignment of output mode + + Serial.print ("powerChannel is set to "); + if (powerChannel == GRID) { + Serial.println ( "grid (CT1)"); } + else { + Serial.println ( "diverted (CT2)"); } + + #ifdef PIN_SAVING_HARDWARE + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // control lines for the 74HC4543 7-seg display driver and the DP line + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + // control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } +#else + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + pinMode(segmentDrivePin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + pinMode(digitSelectorPin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + digitalWrite(digitSelectorPin[i], DIGIT_DISABLED); } + + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + digitalWrite(segmentDrivePin[i], OFF); } +#endif + + // For converting Integer Energy Units into Joules + IEU_per_Joule_grid = (long)CYCLES_PER_SECOND * (1/powerCal_grid); + IEU_per_Joule_diverted = (long)CYCLES_PER_SECOND * (1/powerCal_diverted); + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + + Serial.println ("----"); +} + +// An Interrupt Service Routine is now defined in which the ADC is instructed to +// perform the three analogue measurements in sequence: Voltage, I_diverted and I_grid. +// A "data ready" flag is set after each voltage conversion has been completed. This +// flag is cleared within loop(). +// This Interrupt Service Routine is executed whenever the ADC timer expires. The next +// ADC conversion is initiated from within this ISR. +// +void timerIsr(void) +{ + static unsigned char sample_index = 0; + + switch(sample_index) + { + case 0: + sampleV = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current + ADCSRA |= (1< 0) { + polarityNow = POSITIVE; } + else { + polarityNow = NEGATIVE; } + + if (polarityNow == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + cycleCount++; + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / samplesDuringThisCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / samplesDuringThisCycle; // proportional to Watts + // + // The per-mainsCycle variables can now be reset for ongoing use + samplesDuringThisCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling intervals are of equal duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Add these energy contributions to the relevant accumulator. + energyThisSecond_grid += realEnergy_grid; + energyThisSecond_diverted += realEnergy_diverted; + + // Once per second, the contents of the selected accumulator is + // converted to Joules and displayed as Watts. Both accumulators + // are then reset. + // + perSecondCounter++; + if(perSecondCounter >= CYCLES_PER_SECOND) + { + perSecondCounter = 0; + int powerInWatts; + float powerInWatts_float; + + if(powerChannel == GRID) { + powerInWatts = energyThisSecond_grid / IEU_per_Joule_grid; + powerInWatts_float = (float)energyThisSecond_grid / IEU_per_Joule_grid; } + else { + powerInWatts = energyThisSecond_diverted / IEU_per_Joule_diverted; + powerInWatts_float = (float)energyThisSecond_diverted / IEU_per_Joule_diverted; } + + configureValueForDisplay(powerInWatts); + Serial.println (powerInWatts_float); + + energyThisSecond_grid = 0; + energyThisSecond_diverted = 0; + } + + } // end of processing that is specific to the first Vsample in each +ve half cycle + + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > startUpPeriod * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + samplesDuringThisCycle = 0; + Serial.println ("Go!"); + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12); + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + checkPowerChannelSelection(); // updates outputMode if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is negative + + // processing for EVERY pair of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform +// long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform +// long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + samplesDuringThisCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityOfLastSampleV = polarityNow; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + + +// this function changes the value of powerChannel if the state of the external switch is altered +void checkPowerChannelSelection() +{ + static byte count = 0; + int pinState = digitalRead(powerChannelSelectorPin); + if (pinState != powerChannel) + { + count++; + } + if (count >= 20) + { + count = 0; + powerChannel = (enum powerChannels)pinState; // change the global variable + Serial.print ("powerChannel selection changed to "); + if (powerChannel == GRID) { + Serial.println ( "grid (CT1)"); } + else { + Serial.println ( "diverted (CT2)"); } + } +} + + +// called every second, to update the characters to be displayed +void configureValueForDisplay(int power) +{ + boolean energyValueExceeds10kWh; + boolean powerIsNegative; + int val; + static boolean perSecondToggle = false; + + if (power < 0) { + powerIsNegative = true; + val = -1 * power; } + else { + powerIsNegative = false; + val = power; } + + if (powerIsNegative && !perSecondToggle) + { + // do something different + charsForDisplay[0] = 22; + charsForDisplay[1] = 20; + charsForDisplay[2] = 20; + charsForDisplay[3] = 20; + } + else + { + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + + perSecondToggle = !perSecondToggle; + + Serial.println(power); // in case a display is not available +} + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + // The two versions of the hardware require different logic. + +#ifdef PIN_SAVING_HARDWARE + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } + +#else // PIN_SAVING_HARDWARE + + // This version is more straightforward because the digit-enable lines can be + // used to mask out all of the transitory states, including the Decimal Point. + // The sequence is: + // + // 1. de-activate the digit-enable line that was previously active + // 2. determine the next location which is to be active + // 3. determine the relevant character for the new active location + // 4. set up the segment drivers for the character to be displayed (includes the DP) + // 5. activate the digit-enable line for the new active location + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + displayTime_count = 0; + + // 1. de-activate the location which is currently being displayed + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_DISABLED); + + // 2. determine the next digit location which is to be displayed + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 3. determine the relevant character for the new active location + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 4. set up the segment drivers for the character to be displayed (includes the DP) + for (byte segment = 0; segment < noOfSegmentsPerDigit; segment++) { + byte segmentState = segMap[digitVal][segment]; + digitalWrite( segmentDrivePin[segment], segmentState); } + + // 5. activate the digit-enable line for the new active location + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_ENABLED); + } +#endif // PIN_SAVING_HARDWARE + +} // end of refreshDisplay() + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/cal_bothDisplays_3.ino b/docs/routers/mk2pvrouter.co.uk/cal_bothDisplays_3.ino new file mode 100644 index 0000000..37468ea --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/cal_bothDisplays_3.ino @@ -0,0 +1,784 @@ +/* cal_bothDisplays_3.ino + * + * This sketch provides an easy way of calibrating the current sensors that are + * facilitated via the CT1 and CT2 ports. Channel selection is provided by a + * switch at the "mode" connector (digital IO port D3) on the control board. + * The default selection is CT1; CT2 is selected when the switch is closed. + * The measured value is shown on the 4-digit display, and also at the Serial Monitor. + * + * CT1 normally monitors the power at the grid supply point. + * CT2 normaly monitors power that is sent to the dump load(s). + * + * For this test, the selected CT should be clipped around a lead through which a known + * amount of power is flowing. This can be compared against the displayed value + * which is proportional to the powerCal setting. Once the optimal powerCal values + * have been obtained for each channel, these values can be transferred into the + * main Mk2 PV Router sketch. + * + * Depending on which way around the CT is connected, the measured value may be + * positive or negative. If it is negative, the display will either flash or + * display a negative symbol. Its behaviour will depend on the way that the display + * has been configured. + * + * The 4-digit display can be driven in two different ways, one with an extra pair + * of logic chips, and one without. The appropriate version of the sketch must be + * selected by including or commenting out the "#define PIN_SAVING_HARDWARE" + * statement near the top of the code. + * + * With the pin-saving logic, the display is not able to show a '-' symbol. But + * in the alternative mode, it is. + * + * December 2017, upgraded to cal_bothDisplays_2: + * In the original version, the mains cycle counter cycled through the values 0 to + * CYCLES_PER_SECOND inclusive so data was only processed every (CYCLES_PER_SECOND + 1) + * mains cycles. Because the accumulated energy data was divided by CYCLES_PER_SECOND, + * there was an error of approx 2% in the displayed power value. In version 2, + * the logic has been corrected to avoid this error. + * + * June 2022, upgraded to cal_bothDisplays_3: + * The 4-digit display still shows the power that is measured on the selected CT channel. The + * Serial monitor however now shows the average power at both CT channels. Previously, the value + * for the selected channel was shown twice. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + * June 2022 + */ + +#include + +#include +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +// The two versions of the hardware require different logic. The following line should +// be included if the additional logic chips are present, or excluded if they are +// absent (in which case some wire links need to be fitted) +// +//#define PIN_SAVING_HARDWARE + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum triacStates {TRIAC_ON, TRIAC_OFF}; // the external trigger device is active low +enum powerChannels {DIVERTED, GRID}; + +// The selected channel is determined in realtime via an external switch +enum powerChannels powerChannel; + +// allocation of digital pins (excluding the display which is dealt with separately) +// ****************************************************** +// D0 & D1 are reserved for the Serial i/f +const byte powerChannelSelectorPin = 3; // <-- with the internal pullup + +// allocation of analogue pins (excluding the display which is dealt with separately) +// *************************** +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + + +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long cycleCount = 0; // counts mains cycles from start-up +long energyThisSecond_grid; +long energyThisSecond_diverted; +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long IEU_per_Joule_grid; +long IEU_per_Joule_diverted; + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +int sampleI_grid; +int sampleI_diverted; +int sampleV; + + +// Calibration +//------------ +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// With my PCB-based hardware, the ADC has an input range of 0 to 3.3V and an +// output range of 0 to 1023. The purpose of each input sensor is to +// convert the measured parameter into a low-voltage signal which fits nicely +// within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal of +// the ADC by around a factor of twenty. The conversion rate of the overall system +// for measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.05; // for CT1 +const float powerCal_diverted = 0.05; // for CT2 + + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 23; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates + +// The two versions of the hardware require different logic. +#ifdef PIN_SAVING_HARDWARE + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. In this version, +// the decimal point has to be treated differently than the other seven segments, so +// a convenient means of accessing this column is provided. +// +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH, // '.' <- element 21 + HIGH, HIGH, HIGH, HIGH, LOW // ' ' <- element 22 (for compatibility) +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + +#else // PIN_SAVING_HARDWARE + +#define ON HIGH +#define OFF LOW + +const byte noOfSegmentsPerDigit = 8; // includes one for the decimal point +enum digitEnableStates {DIGIT_ENABLED, DIGIT_DISABLED}; + +byte digitSelectorPin[noOfDigitLocations] = {16,10,13,11}; +byte segmentDrivePin[noOfSegmentsPerDigit] = {2,5,12,6,7,9,8,14}; + +// The final column of segMap[] is for the decimal point status. In this version, +// the decimal point is treated just like all the other segments, so there is +// no need to access this column specifically. +// +byte segMap[noOfPossibleCharacters][noOfSegmentsPerDigit] = { + ON , ON , ON , ON , ON , ON , OFF, OFF, // '0' <- element 0 + OFF, ON , ON , OFF, OFF, OFF, OFF, OFF, // '1' <- element 1 + ON , ON , OFF, ON , ON , OFF, ON , OFF, // '2' <- element 2 + ON , ON , ON , ON , OFF, OFF, ON , OFF, // '3' <- element 3 + OFF, ON , ON , OFF, OFF, ON , ON , OFF, // '4' <- element 4 + ON , OFF, ON , ON , OFF, ON , ON , OFF, // '5' <- element 5 + ON , OFF, ON , ON , ON , ON , ON , OFF, // '6' <- element 6 + ON , ON , ON , OFF, OFF, OFF, OFF, OFF, // '7' <- element 7 + ON , ON , ON , ON , ON , ON , ON , OFF, // '8' <- element 8 + ON , ON , ON , ON , OFF, ON , ON , OFF, // '9' <- element 9 + ON , ON , ON , ON , ON , ON , OFF, ON , // '0.' <- element 10 + OFF, ON , ON , OFF, OFF, OFF, OFF, ON , // '1.' <- element 11 + ON , ON , OFF, ON , ON , OFF, ON , ON , // '2.' <- element 12 + ON , ON , ON , ON , OFF, OFF, ON , ON , // '3.' <- element 13 + OFF, ON , ON , OFF, OFF, ON , ON , ON , // '4.' <- element 14 + ON , OFF, ON , ON , OFF, ON , ON , ON , // '5.' <- element 15 + ON , OFF, ON , ON , ON , ON , ON , ON , // '6.' <- element 16 + ON , ON , ON , OFF, OFF, OFF, OFF, ON , // '7.' <- element 17 + ON , ON , ON , ON , ON , ON , ON , ON , // '8.' <- element 18 + ON , ON , ON , ON , OFF, ON , ON , ON , // '9.' <- element 19 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, // ' ' <- element 20 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, ON , // '.' <- element 21 + OFF, OFF, OFF, OFF, OFF, OFF, ON , OFF // '-' <- element 22 +}; +#endif // PIN_SAVING_HARDWARE + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + + +void setup() +{ + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: cal_bothDisplays_3.ino"); + Serial.println(); + + pinMode(powerChannelSelectorPin, INPUT); + digitalWrite(powerChannelSelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(powerChannelSelectorPin); // initial selection and + powerChannel = (enum powerChannels)pinState; // assignment of output mode + + Serial.print ("powerChannel is set to "); + if (powerChannel == GRID) { + Serial.println ( "grid (CT1)"); } + else { + Serial.println ( "diverted (CT2)"); } + + #ifdef PIN_SAVING_HARDWARE + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // control lines for the 74HC4543 7-seg display driver and the DP line + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + // control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } +#else + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + pinMode(segmentDrivePin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + pinMode(digitSelectorPin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + digitalWrite(digitSelectorPin[i], DIGIT_DISABLED); } + + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + digitalWrite(segmentDrivePin[i], OFF); } +#endif + + // For converting Integer Energy Units into Joules + IEU_per_Joule_grid = (long)CYCLES_PER_SECOND * (1/powerCal_grid); + IEU_per_Joule_diverted = (long)CYCLES_PER_SECOND * (1/powerCal_diverted); + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + + Serial.println ("----"); +} + +// An Interrupt Service Routine is now defined in which the ADC is instructed to +// perform the three analogue measurements in sequence: Voltage, I_diverted and I_grid. +// A "data ready" flag is set after each voltage conversion has been completed. This +// flag is cleared within loop(). +// This Interrupt Service Routine is executed whenever the ADC timer expires. The next +// ADC conversion is initiated from within this ISR. +// +void timerIsr(void) +{ + static unsigned char sample_index = 0; + + switch(sample_index) + { + case 0: + sampleV = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current + ADCSRA |= (1< 0) { + polarityNow = POSITIVE; } + else { + polarityNow = NEGATIVE; } + + if (polarityNow == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + cycleCount++; + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / samplesDuringThisCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / samplesDuringThisCycle; // proportional to Watts + // + // The per-mainsCycle variables can now be reset for ongoing use + samplesDuringThisCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling intervals are of equal duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + + // Add these energy contributions to the relevant accumulator. + energyThisSecond_grid += realEnergy_grid; + energyThisSecond_diverted += realEnergy_diverted; + + // Once per second, the contents of the selected accumulator is + // converted to Joules and displayed as Watts. Both accumulators + // are then reset. + // + perSecondCounter++; + if(perSecondCounter >= CYCLES_PER_SECOND) + { + perSecondCounter = 0; + int powerInWatts_grid; + int powerInWatts_diverted; + + powerInWatts_grid = energyThisSecond_grid / IEU_per_Joule_grid; + powerInWatts_diverted = energyThisSecond_diverted / IEU_per_Joule_diverted; + + Serial.print ("CT1: "); + Serial.print (powerInWatts_grid); + Serial.print ("; CT2: "); + Serial.println (powerInWatts_diverted); + + if(powerChannel == GRID) { + configureValueForDisplay(powerInWatts_grid); } + else { + configureValueForDisplay(powerInWatts_diverted); } + + energyThisSecond_grid = 0; + energyThisSecond_diverted = 0; + } + + } // end of processing that is specific to the first Vsample in each +ve half cycle + + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > startUpPeriod * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + samplesDuringThisCycle = 0; + Serial.println ("Go!"); + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12); + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + checkPowerChannelSelection(); // updates outputMode if switch is changed + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is negative + + // processing for EVERY pair of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the grid current waveform +// long phaseShiftedSampleVminusDC_grid = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_grid_int)>>8); + long phaseShiftedSampleVminusDC_grid = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleVminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // phase-shift the voltage waveform so that it aligns with the diverted current waveform +// long phaseShiftedSampleVminusDC_diverted = lastSampleVminusDC_long +// + (((sampleVminusDC_long - lastSampleVminusDC_long)*phaseCal_diverted_int)>>8); + long phaseShiftedSampleVminusDC_diverted = sampleVminusDC_long; // <- simple version for when + // phaseCal is not in use + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtV_div4 = phaseShiftedSampleVminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + samplesDuringThisCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + lastSampleVminusDC_long = sampleVminusDC_long; // required for phaseCal algorithm + polarityOfLastSampleV = polarityNow; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + + +// this function changes the value of powerChannel if the state of the external switch is altered +void checkPowerChannelSelection() +{ + static byte count = 0; + int pinState = digitalRead(powerChannelSelectorPin); + if (pinState != powerChannel) + { + count++; + } + if (count >= 20) + { + count = 0; + powerChannel = (enum powerChannels)pinState; // change the global variable + Serial.print ("powerChannel selection changed to "); + if (powerChannel == GRID) { + Serial.println ( "grid (CT1)"); } + else { + Serial.println ( "diverted (CT2)"); } + } +} + + +// called every second, to update the characters to be displayed +void configureValueForDisplay(int power) +{ + boolean energyValueExceeds10kWh; + boolean powerIsNegative; + int val; + static boolean perSecondToggle = false; + + if (power < 0) { + powerIsNegative = true; + val = -1 * power; } + else { + powerIsNegative = false; + val = power; } + + if (powerIsNegative && !perSecondToggle) + { + // do something different + charsForDisplay[0] = 22; + charsForDisplay[1] = 20; + charsForDisplay[2] = 20; + charsForDisplay[3] = 20; + } + else + { + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + + perSecondToggle = !perSecondToggle; +} + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + // The two versions of the hardware require different logic. + +#ifdef PIN_SAVING_HARDWARE + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } + +#else // PIN_SAVING_HARDWARE + + // This version is more straightforward because the digit-enable lines can be + // used to mask out all of the transitory states, including the Decimal Point. + // The sequence is: + // + // 1. de-activate the digit-enable line that was previously active + // 2. determine the next location which is to be active + // 3. determine the relevant character for the new active location + // 4. set up the segment drivers for the character to be displayed (includes the DP) + // 5. activate the digit-enable line for the new active location + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + displayTime_count = 0; + + // 1. de-activate the location which is currently being displayed + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_DISABLED); + + // 2. determine the next digit location which is to be displayed + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 3. determine the relevant character for the new active location + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 4. set up the segment drivers for the character to be displayed (includes the DP) + for (byte segment = 0; segment < noOfSegmentsPerDigit; segment++) { + byte segmentState = segMap[digitVal][segment]; + digitalWrite( segmentDrivePin[segment], segmentState); } + + // 5. activate the digit-enable line for the new active location + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_ENABLED); + } +#endif // PIN_SAVING_HARDWARE + +} // end of refreshDisplay() + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/demo_bothDisplays.ino b/docs/routers/mk2pvrouter.co.uk/demo_bothDisplays.ino new file mode 100644 index 0000000..f04fada --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/demo_bothDisplays.ino @@ -0,0 +1,376 @@ +/* + * This sketch is to demonstrate the operation of the 4-digit display that + * forms part of my PCB-based Mk2 PV Router hardware. + * + * The hardware can be assembled in two ways, one with an extra pair of logic + * chips, and one without. The appropriate version of the sketch must be selected + * by including or commenting out the "#define PIN_SAVING_HARDWARE" statement + * which is near the top of the code. + * + * The boolean variable, EDD_isActive, is declared immediately before the + * start of setup(). If this variable is set to false, the display shows a + * "walking dots" pattern. If it is set to true, the display slowly counts up + * from its starting value (which is assigned on the line above). + * + * The greatest value which can be displayed is 65535, this being the maximum value + * of an unsigned int. If the value to be displayed exceeds 9999, the decimal point + * is right-shifted by one place. 12345 would therefore be displayed as 12.34. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + * February 2014 + */ + +#include // may not be needed, but it's probably a good idea to include this + +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 3// no of processing loops between display updates + +// The two versions of the hardware require different logic. +//#define PIN_SAVING_HARDWARE + +#ifdef PIN_SAVING_HARDWARE +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. In this version, +// the decimal point has to be treated differently than the other seven segments, so +// a convenient means of accessing this column is provided. +// +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + +#else // PIN_SAVING_HARDWARE + +#define ON HIGH +#define OFF LOW + +const byte noOfSegmentsPerDigit = 8; // includes one for the decimal point +enum digitEnableStates {DIGIT_ENABLED, DIGIT_DISABLED}; + +byte digitSelectorPin[noOfDigitLocations] = {16,10,13,11}; +byte segmentDrivePin[noOfSegmentsPerDigit] = {2,5,12,6,7,9,8,14}; + + +// The final column of segMap[] is for the decimal point status. In this version, +// the decimal point is treated just like all the other segments, so there is +// no need to access this column specifically. +// +byte segMap[noOfPossibleCharacters][noOfSegmentsPerDigit] = { + ON , ON , ON , ON , ON , ON , OFF, OFF, // '0' <- element 0 + OFF, ON , ON , OFF, OFF, OFF, OFF, OFF, // '1' <- element 1 + ON , ON , OFF, ON , ON , OFF, ON , OFF, // '2' <- element 2 + ON , ON , ON , ON , OFF, OFF, ON , OFF, // '3' <- element 3 + OFF, ON , ON , OFF, OFF, ON , ON , OFF, // '4' <- element 4 + ON , OFF, ON , ON , OFF, ON , ON , OFF, // '5' <- element 5 + ON , OFF, ON , ON , ON , ON , ON , OFF, // '6' <- element 6 + ON , ON , ON , OFF, OFF, OFF, OFF, OFF, // '7' <- element 7 + ON , ON , ON , ON , ON , ON , ON , OFF, // '8' <- element 8 + ON , ON , ON , ON , OFF, ON , ON , OFF, // '9' <- element 9 + ON , ON , ON , ON , ON , ON , OFF, ON , // '0.' <- element 10 + OFF, ON , ON , OFF, OFF, OFF, OFF, ON , // '1.' <- element 11 + ON , ON , OFF, ON , ON , OFF, ON , ON , // '2.' <- element 12 + ON , ON , ON , ON , OFF, OFF, ON , ON , // '3.' <- element 13 + OFF, ON , ON , OFF, OFF, ON , ON , ON , // '4.' <- element 14 + ON , OFF, ON , ON , OFF, ON , ON , ON , // '5.' <- element 15 + ON , OFF, ON , ON , ON , ON , ON , ON , // '6.' <- element 16 + ON , ON , ON , OFF, OFF, OFF, OFF, ON , // '7.' <- element 17 + ON , ON , ON , ON , ON , ON , ON , ON , // '8.' <- element 18 + ON , ON , ON , ON , OFF, ON , ON , ON , // '9.' <- element 19 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, // ' ' <- element 20 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, ON // '.' <- element 11 +}; +#endif // PIN_SAVING_HARDWARE + + +byte charsForDisplay[noOfDigitLocations] = {20, 20, 20, 20}; // all blank +unsigned int valueToBeDisplayed = 1234; + +boolean EDD_isActive = true; // energy diversion detection + + +void setup() +{ + Serial.begin(9600); + Serial.println(); + + delay(5000); // allow time to open Serial monitor + + Serial.println(); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: demo_bothDisplays.ino"); + + +#ifdef PIN_SAVING_HARDWARE + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // control lines for the 74HC4543 7-seg display driver and the DP line + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + // control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } +#else + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + pinMode(segmentDrivePin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + pinMode(digitSelectorPin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + digitalWrite(digitSelectorPin[i], DIGIT_DISABLED); } + + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + digitalWrite(segmentDrivePin[i], OFF); } +#endif + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + + +void loop() +{ + static unsigned long timeOfLastDisplayChange = 0; + + unsigned long timeNow = millis(); + if (timeNow - timeOfLastDisplayChange > 1000) + { + timeOfLastDisplayChange = timeNow; + valueToBeDisplayed++; + configureValueForDisplay(); + } + + delay(1); // to simulate one iteration of loop() in the Mk2 sketch + refreshDisplay(); +} + + +// called every second, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + +// Serial.println(valueToBeDisplayed); + + if (EDD_isActive) + { + unsigned int val = valueToBeDisplayed; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } + +/* + Serial.print(charsForDisplay[0]); + Serial.print(" "); + Serial.print(charsForDisplay[1]); + Serial.print(" "); + Serial.print(charsForDisplay[2]); + Serial.print(" "); + Serial.print(charsForDisplay[3]); + Serial.println(); +*/ +} + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + // The two versions of the hardware require different logic. + +#ifdef PIN_SAVING_HARDWARE + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } + +#else // PIN_SAVING_HARDWARE + + // This version is more straightforward because the digit-enable lines can be + // used to mask out all of the transitory states, including the Decimal Point. + // The sequence is: + // + // 1. de-activate the digit-enable line that was previously active + // 2. determine the next location which is to be active + // 3. determine the relevant character for the new active location + // 4. set up the segment drivers for the character to be displayed (includes the DP) + // 5. activate the digit-enable line for the new active location + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + displayTime_count = 0; + + // 1. de-activate the location which is currently being displayed + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_DISABLED); + + // 2. determine the next digit location which is to be displayed + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 3. determine the relevant character for the new active location + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 4. set up the segment drivers for the character to be displayed (includes the DP) + for (byte segment = 0; segment < noOfSegmentsPerDigit; segment++) { + byte segmentState = segMap[digitVal][segment]; + digitalWrite( segmentDrivePin[segment], segmentState); } + + // 5. activate the digit-enable line for the new active location + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_ENABLED); + } +#endif // PIN_SAVING_HARDWARE + +} // end of refreshDisplay() + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + diff --git a/docs/routers/mk2pvrouter.co.uk/internalWiring_1.pdf b/docs/routers/mk2pvrouter.co.uk/internalWiring_1.pdf new file mode 100644 index 0000000..bd1753e Binary files /dev/null and b/docs/routers/mk2pvrouter.co.uk/internalWiring_1.pdf differ diff --git a/docs/routers/mk2pvrouter.co.uk/mainBoard_Circuit_1.pdf b/docs/routers/mk2pvrouter.co.uk/mainBoard_Circuit_1.pdf new file mode 100644 index 0000000..46619ab Binary files /dev/null and b/docs/routers/mk2pvrouter.co.uk/mainBoard_Circuit_1.pdf differ diff --git a/docs/routers/mk2pvrouter.co.uk/main_rev1_circuit_2.pdf b/docs/routers/mk2pvrouter.co.uk/main_rev1_circuit_2.pdf new file mode 100644 index 0000000..2dd9310 Binary files /dev/null and b/docs/routers/mk2pvrouter.co.uk/main_rev1_circuit_2.pdf differ diff --git a/docs/routers/mk2pvrouter.co.uk/main_rev2_cctDiagram.pdf b/docs/routers/mk2pvrouter.co.uk/main_rev2_cctDiagram.pdf new file mode 100644 index 0000000..4dc0aaf Binary files /dev/null and b/docs/routers/mk2pvrouter.co.uk/main_rev2_cctDiagram.pdf differ diff --git a/docs/routers/mk2pvrouter.co.uk/outputBoard_Circuit_1.pdf b/docs/routers/mk2pvrouter.co.uk/outputBoard_Circuit_1.pdf new file mode 100644 index 0000000..3c770e0 Binary files /dev/null and b/docs/routers/mk2pvrouter.co.uk/outputBoard_Circuit_1.pdf differ diff --git a/docs/routers/mk2pvrouter.co.uk/outputBoard_testCircuit_1.pdf b/docs/routers/mk2pvrouter.co.uk/outputBoard_testCircuit_1.pdf new file mode 100644 index 0000000..58c6aac Binary files /dev/null and b/docs/routers/mk2pvrouter.co.uk/outputBoard_testCircuit_1.pdf differ diff --git a/docs/routers/mk2pvrouter.co.uk/remoteUnit_fasterControl_1.ino b/docs/routers/mk2pvrouter.co.uk/remoteUnit_fasterControl_1.ino new file mode 100644 index 0000000..2ceba58 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/remoteUnit_fasterControl_1.ino @@ -0,0 +1,813 @@ +/* remoteUnit_fasterControl_1.ino + * + * This sketch is to control a remote load for a Mk2 PV Router at the receiver end + * of an RF link. If RF transmission is lost, the load is turned off. A repeater + * signal is available at the 'mode' connector. This is intended to drive an LED + * with an appropriate series resistor, e.g. 120R. + * + * The ability to measure and display the amount of energy which has been diverted + * via the remote load is included. For this to happen, one of the live cores + * needs to pass through a CT which connects to the 'CT2' connector. + * + * The 'CT1' connector has been re-used in this sketch to provide a 2-colour + * indication of the state of the RF link. A schematic for this circuit may be + * found immediately below this header. + * + * A persistence-based 4-digit display is supported. When the RFM12B module is + * in use, the display can only be used in conjunction with an extra pair of + * logic chips. These are ICs 3 and 4, which reduce the number of processor pins + * that are needed to drive the display. + * + * This sketch is similar in function to RF_for_Mk2_rx.ino, as posted on the + * OpenEnergyMonitor forum. That version, and other related material, can be + * found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * January 2016: renamed as remote_Mk2_receiver_1a, with a minor change in the ISR to + * remove a timing uncertainty. Support for the RF69 RF module has also been included. + * + * January 2016: updated to remote_Mk2_receiver_1b: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * February 2016: updated to remote_Mk2_receiver_2, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - change all instances of "triac" to "load" + * + * September 2022: updated to remoteUnit_fasterConrol_1, with this change: + * - RF payload reduced to just one integer for the load state. For use with the transmitter + * sketch, Mk2_fasterControl_withRemoteLoad_n + * - the hardware timer that controls the ADC has been increased from 200 to 250 us (just to + * reduce the workload). + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + + /******************************************************* + suggested circuit for the bi-colour RF-status indicator + + ------------------> +3.3V + | + --- + \ / Red LED (to show when the RF link is faulty) + --- + | + / + \ 120R + / + | + |--------> CT1 (the lower pin of the two, + | not Vref which is the upper pin) + | + / + \ 120R + / + | + --- + \ / Green LED (to show when the RF link is OK) + --- + | + -----------------> GND + +******************************************************* +*/ + +#define RF69_COMPAT 0 // <-- include this line for the RFM12B +// #define RF69_COMPAT 1 // <-- include this line for the RF69 + +#include +#include // JeeLib is available at from: http://github.com/jcw/jeelib +#include + +#define ADC_TIMER_PERIOD 250 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change this values to suit the local mains frequency +#define CYCLES_PER_SECOND 50 + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum outputModes {ANTI_FLICKER, NORMAL}; + +enum loadStates {LOAD_ON, LOAD_OFF}; // to match Tx protocol, the load is active low ... +enum loadStates loadState; + +enum transmissionStates {RF_FAULT, RF_IS_OK}; // two LEDs are driven from one o/p pin +enum transmissionStates transmissionState; + +/* frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ + */ +#define freq RF12_433MHZ // Use the freq to match the module you have. + +const int TXnodeID = 10; +const int myNode = 15; +const int networkGroup = 210; +const int UNO = 1; // Set to 0 if you're not using the UNO bootloader + +// define the data structure for RF comms +typedef struct +{ int dumpState; +} Rx_struct; +Rx_struct receivedData; // an instance of this type + +unsigned long timeAtLastMessage = 0; +unsigned long timeAtLastTransmissionLostDisplay; + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +// D2 is for the RFM12B +const byte loadIndicator_LED = 3; // <-- active high +const byte outputForTrigger = 4; // <- active low +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +// D10 is for the RFM12B +// D11 is for the RFM12B +// D12 is for the RFM12B +// D13 is for the RFM12B + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte transmissionStatusPin = 19; // A5 is to control a pair of red & green LEDs + +const byte delayBeforeSerialStarts = 3; // in seconds, to allow Serial window to be opened +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long cycleCount = 0; // counts mains cycles from start-up +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; +long mainsCyclesPerHour; + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +volatile int sampleI_diverted; +volatile int sampleV; + + +// Calibration +//------------ +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 3.3V and an output range of 1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is numerically smaller than the +// output signal by around a factor of twenty. The conversion rate of the +// overall system for measuring CURRENT is therefore likely to be around +// 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; + +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of this array is for the decimal point status. +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above array +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // Energy Diversion Detection + + +void setup() +{ + pinMode(outputForTrigger, OUTPUT); + digitalWrite (outputForTrigger, LOAD_OFF); // the external trigger is active low + + pinMode(loadIndicator_LED, OUTPUT); + + pinMode(transmissionStatusPin, OUTPUT); + + delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: remoteUnit_fasterControl_1.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, the energy measurement scale is altered to match the + // energy detection mechanism that is in use. This avoids the need to re-scale + // every energy contribution, thus saving processing time. This process is + // described in more detail in the function, allGeneralProcessing(), at the start + // of each new mains cycle. + // + // Diverted energy data, as measured using CT2, is stored in an 'integer maths' + // accumulator. Whenever its value exceeds 1 Wh, an associated WattHour register + // is incremented, and the accumulator's value is decremented accordingly. The + // calculation below is to determine the correct scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + + Serial.println ("----"); + + delay(1000); +// rf12_set_cs(10); //emonTx, emonGLCD, NanodeRF, JeeNode + + rf12_initialize(myNode, freq, networkGroup); +} + +// An Interrupt Service Routine is now defined in which the ADC is instructed to +// measure V and I alternately. A "data ready" flag is set after each voltage conversion +// has been completed. +// For each pair of samples, this means that current is measured before voltage. The +// current sample is taken first because the phase of the waveform for current is generally +// slightly advanced relative to the waveform for voltage. The data ready flag is cleared +// within loop(). +// This Interrupt Service Routine is for use when the ADC is fixed timer mode. It is +// executed whenever the ADC timer expires. In this mode, the next ADC conversion is +// initiated from within this ISR. +// +void timerIsr(void) +{ + static unsigned char sample_index = 0; + static int sampleI_diverted_raw; + + switch(sample_index) + { + case 0: + sampleV = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current + ADCSRA |= (1< 0) { + polarityNow = POSITIVE; } + else { + polarityNow = NEGATIVE; } + + if (polarityNow == POSITIVE) + { + if (polarityOfLastSampleV != POSITIVE) + { + if (beyondStartUpPhase) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + cycleCount++; + + // update the Energy Diversion Detector which is determined by the + // state of the remote load, as instruction via the RF link + // + if (loadState == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + + if (EDD_isActive) // Energy Diversion Display (EDD) + { + // In this sketch, energy contributions need only be processed if EDD is active. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_diverted = sumP_diverted / samplesDuringThisCycle; // proportional to Watts + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_diverted = realPower_diverted; + + // to avoid 'creep', small energy contributions are ignored + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) { + realEnergy_diverted = 0; } + + // The latest energy contribution needs to be added to an accumulator which operates + // with maximum precision. + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + + // the data to be displayed is configured every second + perSecondCounter++; + if(perSecondCounter >= CYCLES_PER_SECOND) + { + perSecondCounter = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + +/* + Serial.print("Diverted: " ); + Serial.print(divertedEnergyTotal_Wh); + Serial.print(" Wh plus "); + Serial.print((powerCal_diverted / CYCLES_PER_SECOND) * divertedEnergyRecent_IEU); + Serial.print("J, EDD is" ); + if (EDD_isActive) { + Serial.println(" on" ); } + else { + Serial.println(" off" ); } +*/ + configureValueForDisplay(); // occurs every second + } + + // clear the per-cycle accumulators for use in this new mains cycle. + samplesDuringThisCycle = 0; + sumP_diverted = 0; + + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000) + { + beyondStartUpPhase = true; + sumP_diverted = 0; + samplesDuringThisCycle = 0; + Serial.println ("Go!"); + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12); + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is negative + + // processing for EVERY pair of samples + // + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + samplesDuringThisCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + polarityOfLastSampleV = polarityNow; // for identification of half cycle boundaries + + + // Every time that this function is run, a check is performed to find out + // whether any new RF instructions have been received. This occurs every 400 uS. + // + unsigned long timeNow = millis(); // to detect when the RF-link has failed + + if (rf12_recvDone()) + { + if (rf12_crc == 0 && (rf12_hdr & RF12_HDR_CTL) == 0) + { + int node_id = (rf12_hdr & 0x1F); + byte n = rf12_len; + + if (node_id == TXnodeID) + { + receivedData = *(Rx_struct*) rf12_data; + loadState = (enum loadStates)receivedData.dumpState; + + // process load-state data + digitalWrite(outputForTrigger, loadState); // active low, same as Tx protocol + digitalWrite(loadIndicator_LED, !loadState); // active high + + timeAtLastMessage = timeNow; + + } + } + else + { + Serial.println("Corrupt message!"); + } + } + + if ((timeNow - timeAtLastMessage) > 3500) + { + // transmission has been lost + transmissionState = RF_FAULT; + loadState = LOAD_OFF; + digitalWrite(outputForTrigger, loadState); + digitalWrite(loadIndicator_LED, !loadState); + + if(timeNow > timeAtLastTransmissionLostDisplay + 1000) + { + Serial.println("transmission lost!"); + timeAtLastTransmissionLostDisplay = timeNow; + } + } + else + { + transmissionState = RF_IS_OK; + } + + digitalWrite(transmissionStatusPin, transmissionState); + refreshDisplay(); + +} // end of allGeneralProcessing() + + +// called every second, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // a re-scaling is necessary (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +} + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + // + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/remote_Mk2_receiver_1.ino b/docs/routers/mk2pvrouter.co.uk/remote_Mk2_receiver_1.ino new file mode 100644 index 0000000..5667232 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/remote_Mk2_receiver_1.ino @@ -0,0 +1,804 @@ +/* remote_Mk2_receiver_1.ino + * + * This sketch is to control a remote load for a Mk2 PV Router at the receiver end + * of an RF link. If RF transmission is lost, the triac is turned off. A repeater + * signal is available at the 'mode' connector. This is intended to drive an LED + * with an appropriate series resistor, e.g. 120R. + * + * The ability to measure and display the amount of energy which has been diverted + * via the remote load is included. For this to happen, one of the live cores + * needs to pass through a CT which connects to the 'CT2' connector. + * + * The 'CT1' connector has been re-used in this sketch to provide a 2-colour + * indication of the state of the RF link. A schematic for this circuit may be + * found immediately below this header. + * + * A persistence-based 4-digit display is supported. When the RFM12B module is + * in use, the display can only be used in conjunction with an extra pair of + * logic chips. These are ICs 3 and 4, which reduce the number of processor pins + * that are needed to drive the display. + * + * This sketch is similar in function to RF_for_Mk2_rx.ino, as posted on the + * OpenEnergyMonitor forum. That version, and other related material, can be + * found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * Robin Emley + * www.Mk2PVrouter.co.uk + * April 2014 + */ + + /******************************************************* + suggested circuit for the bi-colour RF-status indicator + + ------------------> +3.3V + | + --- + \ / Red LED (to show when the RF link is faulty) + --- + | + / + \ 120R + / + | + |--------> CT1 (the lower pin of the two, + | not Vref which is the upper pin) + | + / + \ 120R + / + | + --- + \ / Green LED (to show when the RF link is OK) + --- + | + -----------------> GND + +******************************************************* +*/ + + +#include +#include // JeeLib is available at from: http://github.com/jcw/jeelib +#include + +#define ADC_TIMER_PERIOD 200 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change this values to suit the local mains frequency +#define CYCLES_PER_SECOND 50 + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +//enum triacStates {TRIAC_ON, TRIAC_OFF}; // the external trigger device is active low +enum outputModes {ANTI_FLICKER, NORMAL}; + +enum loadStates {LOAD_ON, LOAD_OFF}; // to match Tx protocol, the load is active low ... +enum loadStates loadState; + +enum transmissionStates {RF_FAULT, RF_IS_OK}; // two LEDs are driven from one o/p pin +enum transmissionStates transmissionState; + +/* frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ + */ +#define freq RF12_868MHZ // Use the freq to match the module you have. + +const int TXnodeID = 10; +const int myNode = 15; +const int networkGroup = 210; +const int UNO = 1; // Set to 0 if you're not using the UNO bootloader + +// define the data structure for RF comms +typedef struct +{ byte dumpState; + int msgNumber; +} Rx_struct; +Rx_struct receivedData; // an instance of this type + +unsigned long timeAtLastMessage = 0; +int lastMsgNumber = 0; +unsigned long timeAtLastTransmissionLostDisplay; + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +// D2 is for the RFM12B +const byte loadIndicator_LED = 3; // <-- active high +const byte outputForTrigger = 4; // <- active low +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +// D10 is for the RFM12B +// D11 is for the RFM12B +// D12 is for the RFM12B +// D13 is for the RFM12B + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte transmissionStatusPin = 19; // A5 is to control a pair of red & green LEDs + + +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long cycleCount = 0; // counts mains cycles from start-up +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; +long mainsCyclesPerHour; + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +int sampleI_diverted; +int sampleV; + + +// Calibration +//------------ +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 3.3V and an output range of 1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is numerically smaller than the +// output signal by around a factor of twenty. The conversion rate of the +// overall system for measuring CURRENT is therefore likely to be around +// 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; + +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 10 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of this array is for the decimal point status. +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above array +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // Energy Diversion Detection + + +void setup() +{ + pinMode(outputForTrigger, OUTPUT); + digitalWrite (outputForTrigger, LOAD_OFF); // the external trigger is active low + + pinMode(loadIndicator_LED, OUTPUT); +// digitalWrite (outputForTrigger, LOAD_OFF); // the external trigger is active low + + pinMode(transmissionStatusPin, OUTPUT); +// digitalWrite (outputForTrigger, TRIAC_OFF); // the external trigger is active low + + delay(5000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: remote_Mk2_receiver_1.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, the energy measurement scale is altered to match the + // energy detection mechanism that is in use. This avoids the need to re-scale + // every energy contribution, thus saving processing time. This process is + // described in more detail in the function, allGeneralProcessing(), at the start + // of each new mains cycle. + // + // Diverted energy data, as measured using CT2, is stored in an 'integer maths' + // accumulator. Whenever its value exceeds 1 Wh, an associated WattHour register + // is incremented, and the accumulator's value is decremented accordingly. The + // calculation below is to determine the correct scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + + Serial.println ("----"); + + delay(1000); + rf12_set_cs(10); //emonTx, emonGLCD, NanodeRF, JeeNode + + rf12_initialize(myNode, freq, networkGroup); +} + +// An Interrupt Service Routine is now defined in which the ADC is instructed to +// measure V and I alternately. A "data ready" flag is set after each voltage conversion +// has been completed. +// For each pair of samples, this means that current is measured before voltage. The +// current sample is taken first because the phase of the waveform for current is generally +// slightly advanced relative to the waveform for voltage. The data ready flag is cleared +// within loop(). +// This Interrupt Service Routine is for use when the ADC is fixed timer mode. It is +// executed whenever the ADC timer expires. In this mode, the next ADC conversion is +// initiated from within this ISR. +// +void timerIsr(void) +{ + static unsigned char sample_index = 0; + + switch(sample_index) + { + case 0: + sampleV = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current + ADCSRA |= (1< 0) { + polarityNow = POSITIVE; } + else { + polarityNow = NEGATIVE; } + + if (polarityNow == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + cycleCount++; + + // update the Energy Diversion Detector which is determined by the + // state of the remote load, as instruction via the RF link + // + if (loadState == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + + if (EDD_isActive) // Energy Diversion Display (EDD) + { + // In this sketch, energy contributions need only be processed if EDD is active. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_diverted = sumP_diverted / samplesDuringThisCycle; // proportional to Watts + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_diverted = realPower_diverted; + + // to avoid 'creep', small energy contributions are ignored + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) { + realEnergy_diverted = 0; } + + // The latest energy contribution needs to be added to an accumulator which operates + // with maximum precision. + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + + // the data to be displayed is configured every second + perSecondCounter++; + if(perSecondCounter >= CYCLES_PER_SECOND) + { + perSecondCounter = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + +/* + Serial.print("Diverted: " ); + Serial.print(divertedEnergyTotal_Wh); + Serial.print(" Wh plus "); + Serial.print((powerCal_diverted / CYCLES_PER_SECOND) * divertedEnergyRecent_IEU); + Serial.print("J, EDD is" ); + if (EDD_isActive) { + Serial.println(" on" ); } + else { + Serial.println(" off" ); } +*/ + configureValueForDisplay(); // occurs every second + } + + // clear the per-cycle accumulators for use in this new mains cycle. + samplesDuringThisCycle = 0; + sumP_diverted = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > startUpPeriod * 1000) + { + beyondStartUpPhase = true; + sumP_diverted = 0; + samplesDuringThisCycle = 0; + Serial.println ("Go!"); + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is negative + + // processing for EVERY pair of samples + // + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + samplesDuringThisCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + polarityOfLastSampleV = polarityNow; // for identification of half cycle boundaries + + + // Every time that this function is run, a check is performed to find out + // whether any new RF instructions have been received. This occurs every 400 uS. + // + unsigned long timeNow = millis(); // to detect when the RF-link has failed + + if (rf12_recvDone()) + { + if (rf12_crc == 0 && (rf12_hdr & RF12_HDR_CTL) == 0) + { + int node_id = (rf12_hdr & 0x1F); + byte n = rf12_len; + + if (node_id == TXnodeID) + { + receivedData = *(Rx_struct*) rf12_data; + loadState = (enum loadStates)receivedData.dumpState; + + // process load-state data + digitalWrite(outputForTrigger, loadState); // active low, same as Tx protocol + digitalWrite(loadIndicator_LED, !loadState); // active high + + // process message number data + byte msgNumber = receivedData.msgNumber; + if ((msgNumber != lastMsgNumber + 1) || + ((msgNumber == 0) && (lastMsgNumber != 255))) + { + Serial.println("Message numbering error!"); + } +// + Serial.print(msgNumber); + Serial.print(", "); + Serial.println(loadState); +// + timeAtLastMessage = timeNow; + lastMsgNumber = msgNumber; + + } + } + else + { + Serial.println("Corrupt message!"); + } + } + + if ((timeNow - timeAtLastMessage) > 3500) + { + // transmission has been lost + transmissionState = RF_FAULT; + loadState = LOAD_OFF; + digitalWrite(outputForTrigger, loadState); + digitalWrite(loadIndicator_LED, !loadState); + + if(timeNow > timeAtLastTransmissionLostDisplay + 1000) + { + Serial.println("transmission lost!"); + timeAtLastTransmissionLostDisplay = timeNow; + } + } + else + { + transmissionState = RF_IS_OK; + } + + digitalWrite(transmissionStatusPin, transmissionState); + refreshDisplay(); + +} // end of allGeneralProcessing() + + +// called every second, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // a re-scaling is necessary (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +} + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + // + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/remote_Mk2_receiver_1a.ino b/docs/routers/mk2pvrouter.co.uk/remote_Mk2_receiver_1a.ino new file mode 100644 index 0000000..9311ca0 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/remote_Mk2_receiver_1a.ino @@ -0,0 +1,810 @@ +/* remote_Mk2_receiver_1a.ino + * + * This sketch is to control a remote load for a Mk2 PV Router at the receiver end + * of an RF link. If RF transmission is lost, the triac is turned off. A repeater + * signal is available at the 'mode' connector. This is intended to drive an LED + * with an appropriate series resistor, e.g. 120R. + * + * The ability to measure and display the amount of energy which has been diverted + * via the remote load is included. For this to happen, one of the live cores + * needs to pass through a CT which connects to the 'CT2' connector. + * + * The 'CT1' connector has been re-used in this sketch to provide a 2-colour + * indication of the state of the RF link. A schematic for this circuit may be + * found immediately below this header. + * + * A persistence-based 4-digit display is supported. When the RFM12B module is + * in use, the display can only be used in conjunction with an extra pair of + * logic chips. These are ICs 3 and 4, which reduce the number of processor pins + * that are needed to drive the display. + * + * This sketch is similar in function to RF_for_Mk2_rx.ino, as posted on the + * OpenEnergyMonitor forum. That version, and other related material, can be + * found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * January 2016: renamed as remote_Mk2_receiver_1a, with a minor change in the ISR to + * remove a timing uncertainty. Support for the RF69 RF module has also been included. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + + /******************************************************* + suggested circuit for the bi-colour RF-status indicator + + ------------------> +3.3V + | + --- + \ / Red LED (to show when the RF link is faulty) + --- + | + / + \ 120R + / + | + |--------> CT1 (the lower pin of the two, + | not Vref which is the upper pin) + | + / + \ 120R + / + | + --- + \ / Green LED (to show when the RF link is OK) + --- + | + -----------------> GND + +******************************************************* +*/ + +#define RF69_COMPAT 0 // <-- include this line for the RFM12B +// #define RF69_COMPAT 1 // <-- include this line for the RF69 + +#include +#include // JeeLib is available at from: http://github.com/jcw/jeelib +#include + +#define ADC_TIMER_PERIOD 200 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change this values to suit the local mains frequency +#define CYCLES_PER_SECOND 50 + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +//enum triacStates {TRIAC_ON, TRIAC_OFF}; // the external trigger device is active low +enum outputModes {ANTI_FLICKER, NORMAL}; + +enum loadStates {LOAD_ON, LOAD_OFF}; // to match Tx protocol, the load is active low ... +enum loadStates loadState; + +enum transmissionStates {RF_FAULT, RF_IS_OK}; // two LEDs are driven from one o/p pin +enum transmissionStates transmissionState; + +/* frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ + */ +#define freq RF12_433MHZ // Use the freq to match the module you have. + +const int TXnodeID = 10; +const int myNode = 15; +const int networkGroup = 210; +const int UNO = 1; // Set to 0 if you're not using the UNO bootloader + +// define the data structure for RF comms +typedef struct +{ byte dumpState; + int msgNumber; +} Rx_struct; +Rx_struct receivedData; // an instance of this type + +unsigned long timeAtLastMessage = 0; +int lastMsgNumber = 0; +unsigned long timeAtLastTransmissionLostDisplay; + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +// D2 is for the RFM12B +const byte loadIndicator_LED = 3; // <-- active high +const byte outputForTrigger = 4; // <- active low +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +// D10 is for the RFM12B +// D11 is for the RFM12B +// D12 is for the RFM12B +// D13 is for the RFM12B + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte transmissionStatusPin = 19; // A5 is to control a pair of red & green LEDs + + +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long cycleCount = 0; // counts mains cycles from start-up +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; +long mainsCyclesPerHour; + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +int sampleI_diverted; +int sampleV; + + +// Calibration +//------------ +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 3.3V and an output range of 1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is numerically smaller than the +// output signal by around a factor of twenty. The conversion rate of the +// overall system for measuring CURRENT is therefore likely to be around +// 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; + +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of this array is for the decimal point status. +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above array +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // Energy Diversion Detection + + +void setup() +{ + pinMode(outputForTrigger, OUTPUT); + digitalWrite (outputForTrigger, LOAD_OFF); // the external trigger is active low + + pinMode(loadIndicator_LED, OUTPUT); +// digitalWrite (outputForTrigger, LOAD_OFF); // the external trigger is active low + + pinMode(transmissionStatusPin, OUTPUT); +// digitalWrite (outputForTrigger, TRIAC_OFF); // the external trigger is active low + + delay(5000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: remote_Mk2_receiver_1a.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, the energy measurement scale is altered to match the + // energy detection mechanism that is in use. This avoids the need to re-scale + // every energy contribution, thus saving processing time. This process is + // described in more detail in the function, allGeneralProcessing(), at the start + // of each new mains cycle. + // + // Diverted energy data, as measured using CT2, is stored in an 'integer maths' + // accumulator. Whenever its value exceeds 1 Wh, an associated WattHour register + // is incremented, and the accumulator's value is decremented accordingly. The + // calculation below is to determine the correct scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + + Serial.println ("----"); + + delay(1000); +// rf12_set_cs(10); //emonTx, emonGLCD, NanodeRF, JeeNode + + rf12_initialize(myNode, freq, networkGroup); +} + +// An Interrupt Service Routine is now defined in which the ADC is instructed to +// measure V and I alternately. A "data ready" flag is set after each voltage conversion +// has been completed. +// For each pair of samples, this means that current is measured before voltage. The +// current sample is taken first because the phase of the waveform for current is generally +// slightly advanced relative to the waveform for voltage. The data ready flag is cleared +// within loop(). +// This Interrupt Service Routine is for use when the ADC is fixed timer mode. It is +// executed whenever the ADC timer expires. In this mode, the next ADC conversion is +// initiated from within this ISR. +// +void timerIsr(void) +{ + static unsigned char sample_index = 0; + static int sampleI_diverted_raw; + + switch(sample_index) + { + case 0: + sampleV = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current + ADCSRA |= (1< 0) { + polarityNow = POSITIVE; } + else { + polarityNow = NEGATIVE; } + + if (polarityNow == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + cycleCount++; + + // update the Energy Diversion Detector which is determined by the + // state of the remote load, as instruction via the RF link + // + if (loadState == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + + if (EDD_isActive) // Energy Diversion Display (EDD) + { + // In this sketch, energy contributions need only be processed if EDD is active. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_diverted = sumP_diverted / samplesDuringThisCycle; // proportional to Watts + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_diverted = realPower_diverted; + + // to avoid 'creep', small energy contributions are ignored + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) { + realEnergy_diverted = 0; } + + // The latest energy contribution needs to be added to an accumulator which operates + // with maximum precision. + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + + // the data to be displayed is configured every second + perSecondCounter++; + if(perSecondCounter >= CYCLES_PER_SECOND) + { + perSecondCounter = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + +/* + Serial.print("Diverted: " ); + Serial.print(divertedEnergyTotal_Wh); + Serial.print(" Wh plus "); + Serial.print((powerCal_diverted / CYCLES_PER_SECOND) * divertedEnergyRecent_IEU); + Serial.print("J, EDD is" ); + if (EDD_isActive) { + Serial.println(" on" ); } + else { + Serial.println(" off" ); } +*/ + configureValueForDisplay(); // occurs every second + } + + // clear the per-cycle accumulators for use in this new mains cycle. + samplesDuringThisCycle = 0; + sumP_diverted = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > startUpPeriod * 1000) + { + beyondStartUpPhase = true; + sumP_diverted = 0; + samplesDuringThisCycle = 0; + Serial.println ("Go!"); + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is negative + + // processing for EVERY pair of samples + // + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + samplesDuringThisCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + polarityOfLastSampleV = polarityNow; // for identification of half cycle boundaries + + + // Every time that this function is run, a check is performed to find out + // whether any new RF instructions have been received. This occurs every 400 uS. + // + unsigned long timeNow = millis(); // to detect when the RF-link has failed + + if (rf12_recvDone()) + { + if (rf12_crc == 0 && (rf12_hdr & RF12_HDR_CTL) == 0) + { + int node_id = (rf12_hdr & 0x1F); + byte n = rf12_len; + + if (node_id == TXnodeID) + { + receivedData = *(Rx_struct*) rf12_data; + loadState = (enum loadStates)receivedData.dumpState; + + // process load-state data + digitalWrite(outputForTrigger, loadState); // active low, same as Tx protocol + digitalWrite(loadIndicator_LED, !loadState); // active high + + // process message number data + byte msgNumber = receivedData.msgNumber; + if ((msgNumber != lastMsgNumber + 1) || + ((msgNumber == 0) && (lastMsgNumber != 255))) + { + Serial.println("Message numbering error!"); + } +// + Serial.print(msgNumber); + Serial.print(", "); + Serial.println(loadState); +// + timeAtLastMessage = timeNow; + lastMsgNumber = msgNumber; + + } + } + else + { + Serial.println("Corrupt message!"); + } + } + + if ((timeNow - timeAtLastMessage) > 3500) + { + // transmission has been lost + transmissionState = RF_FAULT; + loadState = LOAD_OFF; + digitalWrite(outputForTrigger, loadState); + digitalWrite(loadIndicator_LED, !loadState); + + if(timeNow > timeAtLastTransmissionLostDisplay + 1000) + { + Serial.println("transmission lost!"); + timeAtLastTransmissionLostDisplay = timeNow; + } + } + else + { + transmissionState = RF_IS_OK; + } + + digitalWrite(transmissionStatusPin, transmissionState); + refreshDisplay(); + +} // end of allGeneralProcessing() + + +// called every second, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // a re-scaling is necessary (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +} + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + // + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/remote_Mk2_receiver_1b.ino b/docs/routers/mk2pvrouter.co.uk/remote_Mk2_receiver_1b.ino new file mode 100644 index 0000000..33b509f --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/remote_Mk2_receiver_1b.ino @@ -0,0 +1,814 @@ +/* remote_Mk2_receiver_1b.ino + * + * This sketch is to control a remote load for a Mk2 PV Router at the receiver end + * of an RF link. If RF transmission is lost, the triac is turned off. A repeater + * signal is available at the 'mode' connector. This is intended to drive an LED + * with an appropriate series resistor, e.g. 120R. + * + * The ability to measure and display the amount of energy which has been diverted + * via the remote load is included. For this to happen, one of the live cores + * needs to pass through a CT which connects to the 'CT2' connector. + * + * The 'CT1' connector has been re-used in this sketch to provide a 2-colour + * indication of the state of the RF link. A schematic for this circuit may be + * found immediately below this header. + * + * A persistence-based 4-digit display is supported. When the RFM12B module is + * in use, the display can only be used in conjunction with an extra pair of + * logic chips. These are ICs 3 and 4, which reduce the number of processor pins + * that are needed to drive the display. + * + * This sketch is similar in function to RF_for_Mk2_rx.ino, as posted on the + * OpenEnergyMonitor forum. That version, and other related material, can be + * found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * January 2016: renamed as remote_Mk2_receiver_1a, with a minor change in the ISR to + * remove a timing uncertainty. Support for the RF69 RF module has also been included. + * + * January 2016: updated to remote_Mk2_receiver_1b: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + + /******************************************************* + suggested circuit for the bi-colour RF-status indicator + + ------------------> +3.3V + | + --- + \ / Red LED (to show when the RF link is faulty) + --- + | + / + \ 120R + / + | + |--------> CT1 (the lower pin of the two, + | not Vref which is the upper pin) + | + / + \ 120R + / + | + --- + \ / Green LED (to show when the RF link is OK) + --- + | + -----------------> GND + +******************************************************* +*/ + +#define RF69_COMPAT 0 // <-- include this line for the RFM12B +// #define RF69_COMPAT 1 // <-- include this line for the RF69 + +#include +#include // JeeLib is available at from: http://github.com/jcw/jeelib +#include + +#define ADC_TIMER_PERIOD 200 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change this values to suit the local mains frequency +#define CYCLES_PER_SECOND 50 + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +//enum triacStates {TRIAC_ON, TRIAC_OFF}; // the external trigger device is active low +enum outputModes {ANTI_FLICKER, NORMAL}; + +enum loadStates {LOAD_ON, LOAD_OFF}; // to match Tx protocol, the load is active low ... +enum loadStates loadState; + +enum transmissionStates {RF_FAULT, RF_IS_OK}; // two LEDs are driven from one o/p pin +enum transmissionStates transmissionState; + +/* frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ + */ +#define freq RF12_433MHZ // Use the freq to match the module you have. + +const int TXnodeID = 10; +const int myNode = 15; +const int networkGroup = 210; +const int UNO = 1; // Set to 0 if you're not using the UNO bootloader + +// define the data structure for RF comms +typedef struct +{ byte dumpState; + int msgNumber; +} Rx_struct; +Rx_struct receivedData; // an instance of this type + +unsigned long timeAtLastMessage = 0; +int lastMsgNumber = 0; +unsigned long timeAtLastTransmissionLostDisplay; + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +// D2 is for the RFM12B +const byte loadIndicator_LED = 3; // <-- active high +const byte outputForTrigger = 4; // <- active low +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +// D10 is for the RFM12B +// D11 is for the RFM12B +// D12 is for the RFM12B +// D13 is for the RFM12B + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte transmissionStatusPin = 19; // A5 is to control a pair of red & green LEDs + + +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long cycleCount = 0; // counts mains cycles from start-up +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; +long mainsCyclesPerHour; + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +volatile int sampleI_diverted; +volatile int sampleV; + + +// Calibration +//------------ +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 3.3V and an output range of 1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is numerically smaller than the +// output signal by around a factor of twenty. The conversion rate of the +// overall system for measuring CURRENT is therefore likely to be around +// 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; + +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of this array is for the decimal point status. +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above array +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // Energy Diversion Detection + + +void setup() +{ + pinMode(outputForTrigger, OUTPUT); + digitalWrite (outputForTrigger, LOAD_OFF); // the external trigger is active low + + pinMode(loadIndicator_LED, OUTPUT); +// digitalWrite (outputForTrigger, LOAD_OFF); // the external trigger is active low + + pinMode(transmissionStatusPin, OUTPUT); +// digitalWrite (outputForTrigger, TRIAC_OFF); // the external trigger is active low + + delay(5000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: remote_Mk2_receiver_1b.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, the energy measurement scale is altered to match the + // energy detection mechanism that is in use. This avoids the need to re-scale + // every energy contribution, thus saving processing time. This process is + // described in more detail in the function, allGeneralProcessing(), at the start + // of each new mains cycle. + // + // Diverted energy data, as measured using CT2, is stored in an 'integer maths' + // accumulator. Whenever its value exceeds 1 Wh, an associated WattHour register + // is incremented, and the accumulator's value is decremented accordingly. The + // calculation below is to determine the correct scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + + Serial.println ("----"); + + delay(1000); +// rf12_set_cs(10); //emonTx, emonGLCD, NanodeRF, JeeNode + + rf12_initialize(myNode, freq, networkGroup); +} + +// An Interrupt Service Routine is now defined in which the ADC is instructed to +// measure V and I alternately. A "data ready" flag is set after each voltage conversion +// has been completed. +// For each pair of samples, this means that current is measured before voltage. The +// current sample is taken first because the phase of the waveform for current is generally +// slightly advanced relative to the waveform for voltage. The data ready flag is cleared +// within loop(). +// This Interrupt Service Routine is for use when the ADC is fixed timer mode. It is +// executed whenever the ADC timer expires. In this mode, the next ADC conversion is +// initiated from within this ISR. +// +void timerIsr(void) +{ + static unsigned char sample_index = 0; + static int sampleI_diverted_raw; + + switch(sample_index) + { + case 0: + sampleV = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current + ADCSRA |= (1< 0) { + polarityNow = POSITIVE; } + else { + polarityNow = NEGATIVE; } + + if (polarityNow == POSITIVE) + { + if (beyondStartUpPhase) + { + if (polarityOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + cycleCount++; + + // update the Energy Diversion Detector which is determined by the + // state of the remote load, as instruction via the RF link + // + if (loadState == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + + if (EDD_isActive) // Energy Diversion Display (EDD) + { + // In this sketch, energy contributions need only be processed if EDD is active. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_diverted = sumP_diverted / samplesDuringThisCycle; // proportional to Watts + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_diverted = realPower_diverted; + + // to avoid 'creep', small energy contributions are ignored + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) { + realEnergy_diverted = 0; } + + // The latest energy contribution needs to be added to an accumulator which operates + // with maximum precision. + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + + // the data to be displayed is configured every second + perSecondCounter++; + if(perSecondCounter >= CYCLES_PER_SECOND) + { + perSecondCounter = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + +/* + Serial.print("Diverted: " ); + Serial.print(divertedEnergyTotal_Wh); + Serial.print(" Wh plus "); + Serial.print((powerCal_diverted / CYCLES_PER_SECOND) * divertedEnergyRecent_IEU); + Serial.print("J, EDD is" ); + if (EDD_isActive) { + Serial.println(" on" ); } + else { + Serial.println(" off" ); } +*/ + configureValueForDisplay(); // occurs every second + } + + // clear the per-cycle accumulators for use in this new mains cycle. + samplesDuringThisCycle = 0; + sumP_diverted = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > startUpPeriod * 1000) + { + beyondStartUpPhase = true; + sumP_diverted = 0; + samplesDuringThisCycle = 0; + Serial.println ("Go!"); + } + } + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>6); // faster than * 0.01 + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is negative + + // processing for EVERY pair of samples + // + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + samplesDuringThisCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + polarityOfLastSampleV = polarityNow; // for identification of half cycle boundaries + + + // Every time that this function is run, a check is performed to find out + // whether any new RF instructions have been received. This occurs every 400 uS. + // + unsigned long timeNow = millis(); // to detect when the RF-link has failed + + if (rf12_recvDone()) + { + if (rf12_crc == 0 && (rf12_hdr & RF12_HDR_CTL) == 0) + { + int node_id = (rf12_hdr & 0x1F); + byte n = rf12_len; + + if (node_id == TXnodeID) + { + receivedData = *(Rx_struct*) rf12_data; + loadState = (enum loadStates)receivedData.dumpState; + + // process load-state data + digitalWrite(outputForTrigger, loadState); // active low, same as Tx protocol + digitalWrite(loadIndicator_LED, !loadState); // active high + + // process message number data + byte msgNumber = receivedData.msgNumber; + if ((msgNumber != lastMsgNumber + 1) || + ((msgNumber == 0) && (lastMsgNumber != 255))) + { + Serial.println("Message numbering error!"); + } +// + Serial.print(msgNumber); + Serial.print(", "); + Serial.println(loadState); +// + timeAtLastMessage = timeNow; + lastMsgNumber = msgNumber; + + } + } + else + { + Serial.println("Corrupt message!"); + } + } + + if ((timeNow - timeAtLastMessage) > 3500) + { + // transmission has been lost + transmissionState = RF_FAULT; + loadState = LOAD_OFF; + digitalWrite(outputForTrigger, loadState); + digitalWrite(loadIndicator_LED, !loadState); + + if(timeNow > timeAtLastTransmissionLostDisplay + 1000) + { + Serial.println("transmission lost!"); + timeAtLastTransmissionLostDisplay = timeNow; + } + } + else + { + transmissionState = RF_IS_OK; + } + + digitalWrite(transmissionStatusPin, transmissionState); + refreshDisplay(); + +} // end of allGeneralProcessing() + + +// called every second, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // a re-scaling is necessary (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +} + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + // + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/remote_Mk2_receiver_2.ino b/docs/routers/mk2pvrouter.co.uk/remote_Mk2_receiver_2.ino new file mode 100644 index 0000000..418634b --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/remote_Mk2_receiver_2.ino @@ -0,0 +1,821 @@ +/* remote_Mk2_receiver_2.ino + * + * This sketch is to control a remote load for a Mk2 PV Router at the receiver end + * of an RF link. If RF transmission is lost, the load is turned off. A repeater + * signal is available at the 'mode' connector. This is intended to drive an LED + * with an appropriate series resistor, e.g. 120R. + * + * The ability to measure and display the amount of energy which has been diverted + * via the remote load is included. For this to happen, one of the live cores + * needs to pass through a CT which connects to the 'CT2' connector. + * + * The 'CT1' connector has been re-used in this sketch to provide a 2-colour + * indication of the state of the RF link. A schematic for this circuit may be + * found immediately below this header. + * + * A persistence-based 4-digit display is supported. When the RFM12B module is + * in use, the display can only be used in conjunction with an extra pair of + * logic chips. These are ICs 3 and 4, which reduce the number of processor pins + * that are needed to drive the display. + * + * This sketch is similar in function to RF_for_Mk2_rx.ino, as posted on the + * OpenEnergyMonitor forum. That version, and other related material, can be + * found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * January 2016: renamed as remote_Mk2_receiver_1a, with a minor change in the ISR to + * remove a timing uncertainty. Support for the RF69 RF module has also been included. + * + * January 2016: updated to remote_Mk2_receiver_1b: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * February 2016: updated to remote_Mk2_receiver_2, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - change all instances of "triac" to "load" + * + * * Robin Emley + * www.Mk2PVrouter.co.uk + */ + + /******************************************************* + suggested circuit for the bi-colour RF-status indicator + + ------------------> +3.3V + | + --- + \ / Red LED (to show when the RF link is faulty) + --- + | + / + \ 120R + / + | + |--------> CT1 (the lower pin of the two, + | not Vref which is the upper pin) + | + / + \ 120R + / + | + --- + \ / Green LED (to show when the RF link is OK) + --- + | + -----------------> GND + +******************************************************* +*/ + +#define RF69_COMPAT 0 // <-- include this line for the RFM12B +// #define RF69_COMPAT 1 // <-- include this line for the RF69 + +#include +#include // JeeLib is available at from: http://github.com/jcw/jeelib +#include + +#define ADC_TIMER_PERIOD 200 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change this values to suit the local mains frequency +#define CYCLES_PER_SECOND 50 + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum outputModes {ANTI_FLICKER, NORMAL}; + +enum loadStates {LOAD_ON, LOAD_OFF}; // to match Tx protocol, the load is active low ... +enum loadStates loadState; + +enum transmissionStates {RF_FAULT, RF_IS_OK}; // two LEDs are driven from one o/p pin +enum transmissionStates transmissionState; + +/* frequency options are RF12_433MHZ, RF12_868MHZ or RF12_915MHZ + */ +#define freq RF12_433MHZ // Use the freq to match the module you have. + +const int TXnodeID = 10; +const int myNode = 15; +const int networkGroup = 210; +const int UNO = 1; // Set to 0 if you're not using the UNO bootloader + +// define the data structure for RF comms +typedef struct +{ byte dumpState; + int msgNumber; +} Rx_struct; +Rx_struct receivedData; // an instance of this type + +unsigned long timeAtLastMessage = 0; +int lastMsgNumber = 0; +unsigned long timeAtLastTransmissionLostDisplay; + +// allocation of digital pins when pin-saving hardware is in use +// ************************************************************* +// D0 & D1 are reserved for the Serial i/f +// D2 is for the RFM12B +const byte loadIndicator_LED = 3; // <-- active high +const byte outputForTrigger = 4; // <- active low +// D5 is the enable line for the 7-segment display driver, IC3 +// D6 is a data input line for the 7-segment display driver, IC3 +// D7 is a data input line for the 7-segment display driver, IC3 +// D8 is a data input line for the 7-segment display driver, IC3 +// D9 is a data input line for the 7-segment display driver, IC3 +// D10 is for the RFM12B +// D11 is for the RFM12B +// D12 is for the RFM12B +// D13 is for the RFM12B + +// allocation of analogue pins +// *************************** +// A0 (D14) is the decimal point driver line for the 4-digit display +// A1 (D15) is a digit selection line for the 4-digit display, via IC4 +// A2 (D16) is a digit selection line for the 4-digit display, via IC4 +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte transmissionStatusPin = 19; // A5 is to control a pair of red & green LEDs + +const byte delayBeforeSerialStarts = 3; // in seconds, to allow Serial window to be opened +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long cycleCount = 0; // counts mains cycles from start-up +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; +long mainsCyclesPerHour; + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +volatile int sampleI_diverted; +volatile int sampleV; + + +// Calibration +//------------ +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "MinAndMaxValues.ino" provides a good starting point for +// system setup. First arrange for the CT to be clipped around either core of a +// cable which supplies a suitable load; then run the tool. The resulting values +// should sit nicely within the range 0-1023. To allow some room for safety, +// a margin of around 100 levels should be left at either end. This gives a +// output range of around 800 ADC levels, which is 80% of its usable range. +// +// My sketch "RawSamplesTool.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 3.3V and an output range of 1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is numerically smaller than the +// output signal by around a factor of twenty. The conversion rate of the +// overall system for measuring CURRENT is therefore likely to be around +// 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; + +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of this array is for the decimal point status. +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above array +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // Energy Diversion Detection + + +void setup() +{ + pinMode(outputForTrigger, OUTPUT); + digitalWrite (outputForTrigger, LOAD_OFF); // the external trigger is active low + + pinMode(loadIndicator_LED, OUTPUT); + + pinMode(transmissionStatusPin, OUTPUT); + + delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: remote_Mk2_receiver_2.ino"); + Serial.println(); + + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + + // When using integer maths, the energy measurement scale is altered to match the + // energy detection mechanism that is in use. This avoids the need to re-scale + // every energy contribution, thus saving processing time. This process is + // described in more detail in the function, allGeneralProcessing(), at the start + // of each new mains cycle. + // + // Diverted energy data, as measured using CT2, is stored in an 'integer maths' + // accumulator. Whenever its value exceeds 1 Wh, an associated WattHour register + // is incremented, and the accumulator's value is decremented accordingly. The + // calculation below is to determine the correct scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + + Serial.println ("----"); + + delay(1000); +// rf12_set_cs(10); //emonTx, emonGLCD, NanodeRF, JeeNode + + rf12_initialize(myNode, freq, networkGroup); +} + +// An Interrupt Service Routine is now defined in which the ADC is instructed to +// measure V and I alternately. A "data ready" flag is set after each voltage conversion +// has been completed. +// For each pair of samples, this means that current is measured before voltage. The +// current sample is taken first because the phase of the waveform for current is generally +// slightly advanced relative to the waveform for voltage. The data ready flag is cleared +// within loop(). +// This Interrupt Service Routine is for use when the ADC is fixed timer mode. It is +// executed whenever the ADC timer expires. In this mode, the next ADC conversion is +// initiated from within this ISR. +// +void timerIsr(void) +{ + static unsigned char sample_index = 0; + static int sampleI_diverted_raw; + + switch(sample_index) + { + case 0: + sampleV = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current + ADCSRA |= (1< 0) { + polarityNow = POSITIVE; } + else { + polarityNow = NEGATIVE; } + + if (polarityNow == POSITIVE) + { + if (polarityOfLastSampleV != POSITIVE) + { + if (beyondStartUpPhase) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + cycleCount++; + + // update the Energy Diversion Detector which is determined by the + // state of the remote load, as instruction via the RF link + // + if (loadState == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + + if (EDD_isActive) // Energy Diversion Display (EDD) + { + // In this sketch, energy contributions need only be processed if EDD is active. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_diverted = sumP_diverted / samplesDuringThisCycle; // proportional to Watts + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step is normally to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient simply to + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_diverted = realPower_diverted; + + // to avoid 'creep', small energy contributions are ignored + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) { + realEnergy_diverted = 0; } + + // The latest energy contribution needs to be added to an accumulator which operates + // with maximum precision. + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + + // the data to be displayed is configured every second + perSecondCounter++; + if(perSecondCounter >= CYCLES_PER_SECOND) + { + perSecondCounter = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + +/* + Serial.print("Diverted: " ); + Serial.print(divertedEnergyTotal_Wh); + Serial.print(" Wh plus "); + Serial.print((powerCal_diverted / CYCLES_PER_SECOND) * divertedEnergyRecent_IEU); + Serial.print("J, EDD is" ); + if (EDD_isActive) { + Serial.println(" on" ); } + else { + Serial.println(" off" ); } +*/ + configureValueForDisplay(); // occurs every second + } + + // clear the per-cycle accumulators for use in this new mains cycle. + samplesDuringThisCycle = 0; + sumP_diverted = 0; + + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000) + { + beyondStartUpPhase = true; + sumP_diverted = 0; + samplesDuringThisCycle = 0; + Serial.println ("Go!"); + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12); + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is negative + + // processing for EVERY pair of samples + // + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + samplesDuringThisCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + polarityOfLastSampleV = polarityNow; // for identification of half cycle boundaries + + + // Every time that this function is run, a check is performed to find out + // whether any new RF instructions have been received. This occurs every 400 uS. + // + unsigned long timeNow = millis(); // to detect when the RF-link has failed + + if (rf12_recvDone()) + { + if (rf12_crc == 0 && (rf12_hdr & RF12_HDR_CTL) == 0) + { + int node_id = (rf12_hdr & 0x1F); + byte n = rf12_len; + + if (node_id == TXnodeID) + { + receivedData = *(Rx_struct*) rf12_data; + loadState = (enum loadStates)receivedData.dumpState; + + // process load-state data + digitalWrite(outputForTrigger, loadState); // active low, same as Tx protocol + digitalWrite(loadIndicator_LED, !loadState); // active high + + // process message number data + byte msgNumber = receivedData.msgNumber; + if ((msgNumber != lastMsgNumber + 1) || + ((msgNumber == 0) && (lastMsgNumber != 255))) + { + Serial.println("Message numbering error!"); + } +// + Serial.print(msgNumber); + Serial.print(", "); + Serial.println(loadState); +// + timeAtLastMessage = timeNow; + lastMsgNumber = msgNumber; + + } + } + else + { + Serial.println("Corrupt message!"); + } + } + + if ((timeNow - timeAtLastMessage) > 3500) + { + // transmission has been lost + transmissionState = RF_FAULT; + loadState = LOAD_OFF; + digitalWrite(outputForTrigger, loadState); + digitalWrite(loadIndicator_LED, !loadState); + + if(timeNow > timeAtLastTransmissionLostDisplay + 1000) + { + Serial.println("transmission lost!"); + timeAtLastTransmissionLostDisplay = timeNow; + } + } + else + { + transmissionState = RF_IS_OK; + } + + digitalWrite(transmissionStatusPin, transmissionState); + refreshDisplay(); + +} // end of allGeneralProcessing() + + +// called every second, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // a re-scaling is necessary (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +} + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + // + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } +} // end of refreshDisplay() + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.co.uk/resultsFrom_RFdatalog_4.txt b/docs/routers/mk2pvrouter.co.uk/resultsFrom_RFdatalog_4.txt new file mode 100644 index 0000000..cfc0d0d --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/resultsFrom_RFdatalog_4.txt @@ -0,0 +1,117 @@ +Some results taken with the sketch Mk2_RFdatalog_4.ino + +Software: This sketch is exactly as posted, except that the persistence value for +zero-crossing detection is set to 1 rather than 2). + +Hardware: A new (green) PCB with dual supplies such that the processor is running at +5V with the RFM12B at 3.3V. + +A datalog message is transmitted every 5 seconds of the form: +typedef struct { + int powerAtSupplyPoint_Watts; // import = +ve, to match OEM convention + int divertedEnergyTotal_Wh; // always positive + int Vrms_times100; + int temperature_times100; +} Tx_struct; + +These values are displayed below along with two additional values which show that the +underlying sampling process is not being disturbed by any of the slower activities +such as Serial, RF or temperature sensing. + +Test sequence: +Stage 1: For the first 20 seconds, no power was flowing at CT1: +- grid power = 0 (Watts) +- diverted energy = 0 WattHours +- Vrms = 222, this value being uncalibrated because voltageCal is just set to 1 +- temperature = 13.25 (yes, it's rather chilly here at this time of year!) +- minSampleSets/MC = 64, because 20000us / (104us * 3) = 64.1 +- #ofSampleSets ~16K because 5000000us / (104us * 3) = 16025 + +Stage 2: For the next 35 seconds, a 750 Watt heater was used to simulate surplus PV: +- grid power = ~780 (Watts) +- diverted energy = 0 Wh because no dumpload was available +- (not sure why the Vrms value has increased slightly here. Maybe the mains voltage changed ...) + +Stage 3: For the next 25 seconds, a 2000 Watt heater was added as a dumpload: +- grid power = variable, as the system maintained the balance between import and export (AF mode) +- diverted energy slowly increased to 5 Wh as "surplus" energy was diverted to the dumpload +- Vrms is reduced as more power is taken from the (very weak) mains supply + +Stage 4: For the next 20 seconds, the dumpload was removed: +- grid power = ~780 (Watts) +- diverted energy remained unchanged because no dumpload was available + +Stage 5: For the remainder of this test, the source of "PV" was removed: +- grid power returned to 0 (Watts) +- diverted energy remained unchanged because no dumpload was available +- Vrms returned to its normal value +- temperature value increased due to hand-warning of sensor. + +Apart from separating this printout to show the different stages of this test sequence, +these results are exactly as obtained from the serial monitor. For ease of comprehension, +the Vrms and temperature values are displayed by the sketch at x1 scale rather than x100 . + +Robin Emley +11th December 2014 + +------------------------------------- +Sketch ID: Mk2_RFdatalog_4.ino + +ADC mode: free-running +Output mode: anti-flicker + offsetOfEnergyThresholds = 0.10 +powerCal_CT1, for grid consumption = 0.0720 +powerCal_CT2, for diverted power = 0.0730 +voltageCal, for Vrms = 1.0000 +Anti-creep limit (Joules / mains cycle) = 5 +Export rate (Watts) = 0 +zero-crossing persistence (sample sets) = 1 +>>free RAM = 784 + capacityOfEnergyBucket_long = 2500000 + lowerEnergyThreshold_long = 1000000 + upperEnergyThreshold_long = 1500000 +>>free RAM = 770 +---- + +Stage 1: +------- +datalog event: grid power 0, diverted energy (Wh) 0, Vrms 281.13, temperature 13.25, (minSampleSets/MC 0, #ofSampleSets 16032) +datalog event: grid power 0, diverted energy (Wh) 0, Vrms 222.75, temperature 13.25, (minSampleSets/MC 64, #ofSampleSets 16032) +datalog event: grid power 0, diverted energy (Wh) 0, Vrms 222.46, temperature 13.25, (minSampleSets/MC 64, #ofSampleSets 16034) +datalog event: grid power 0, diverted energy (Wh) 0, Vrms 222.55, temperature 13.25, (minSampleSets/MC 64, #ofSampleSets 16037) + +Stage 2: +------- +datalog event: grid power 180, diverted energy (Wh) 0, Vrms 222.25, temperature 13.25, (minSampleSets/MC 64, #ofSampleSets 16038) +datalog event: grid power 785, diverted energy (Wh) 0, Vrms 224.43, temperature 13.25, (minSampleSets/MC 64, #ofSampleSets 16036) +datalog event: grid power 785, diverted energy (Wh) 0, Vrms 224.72, temperature 13.25, (minSampleSets/MC 64, #ofSampleSets 16036) +datalog event: grid power 784, diverted energy (Wh) 0, Vrms 224.61, temperature 13.25, (minSampleSets/MC 64, #ofSampleSets 16038) +datalog event: grid power 784, diverted energy (Wh) 0, Vrms 224.59, temperature 13.25, (minSampleSets/MC 64, #ofSampleSets 16039) +datalog event: grid power 784, diverted energy (Wh) 0, Vrms 224.60, temperature 13.25, (minSampleSets/MC 64, #ofSampleSets 16038) +datalog event: grid power 784, diverted energy (Wh) 0, Vrms 224.64, temperature 13.25, (minSampleSets/MC 64, #ofSampleSets 16037) + +Stage 3: +------- +datalog event: grid power -19, diverted energy (Wh) 1, Vrms 222.05, temperature 13.25, (minSampleSets/MC 64, #ofSampleSets 16035) +datalog event: grid power -71, diverted energy (Wh) 2, Vrms 219.82, temperature 13.25, (minSampleSets/MC 64, #ofSampleSets 16035) +datalog event: grid power 132, diverted energy (Wh) 3, Vrms 220.02, temperature 13.25, (minSampleSets/MC 64, #ofSampleSets 16034) +datalog event: grid power -67, diverted energy (Wh) 4, Vrms 220.19, temperature 13.18, (minSampleSets/MC 63, #ofSampleSets 16035) +datalog event: grid power -31, diverted energy (Wh) 5, Vrms 220.17, temperature 13.25, (minSampleSets/MC 63, #ofSampleSets 16034) + +Stage 4: +------- +datalog event: grid power 728, diverted energy (Wh) 5, Vrms 223.50, temperature 13.25, (minSampleSets/MC 64, #ofSampleSets 16032) +datalog event: grid power 783, diverted energy (Wh) 5, Vrms 224.50, temperature 13.25, (minSampleSets/MC 64, #ofSampleSets 16034) +datalog event: grid power 783, diverted energy (Wh) 5, Vrms 224.47, temperature 13.25, (minSampleSets/MC 64, #ofSampleSets 16034) +datalog event: grid power 781, diverted energy (Wh) 5, Vrms 224.19, temperature 13.25, (minSampleSets/MC 64, #ofSampleSets 16036) + +Stage 5: +------- +datalog event: grid power 214, diverted energy (Wh) 5, Vrms 224.95, temperature 13.25, (minSampleSets/MC 64, #ofSampleSets 16034) +datalog event: grid power 0, diverted energy (Wh) 5, Vrms 225.25, temperature 13.25, (minSampleSets/MC 64, #ofSampleSets 16034) +datalog event: grid power 0, diverted energy (Wh) 5, Vrms 225.47, temperature 14.25, (minSampleSets/MC 64, #ofSampleSets 16033) +datalog event: grid power 0, diverted energy (Wh) 5, Vrms 225.43, temperature 20.62, (minSampleSets/MC 64, #ofSampleSets 16031) +datalog event: grid power 0, diverted energy (Wh) 5, Vrms 225.40, temperature 23.18, (minSampleSets/MC 64, #ofSampleSets 16032) +datalog event: grid power 0, diverted energy (Wh) 5, Vrms 225.44, temperature 24.56, (minSampleSets/MC 64, #ofSampleSets 16035) +datalog event: grid power 0, diverted energy (Wh) 5, Vrms 225.51, temperature 25.43, (minSampleSets/MC 64, #ofSampleSets 16036) +datalog event: grid power 0, diverted energy (Wh) 5, Vrms 225.35, temperature 26.06, (minSampleSets/MC 64, #ofSampleSets 16037) diff --git a/docs/routers/mk2pvrouter.co.uk/segCheck_bothDisplays.ino b/docs/routers/mk2pvrouter.co.uk/segCheck_bothDisplays.ino new file mode 100644 index 0000000..d1f60c6 --- /dev/null +++ b/docs/routers/mk2pvrouter.co.uk/segCheck_bothDisplays.ino @@ -0,0 +1,344 @@ +/* + * This sketch exercises every digit of the 4-digit display that forms part of my + * PCB-based Mk2 PV Router hardware. Before fitting a display module into an + * enclosure, this sketch provides an easy way of ensuring that it is fully working. + * + * The hardware that drives the display can be assembled in two ways, one with an + * extra pair of logic chips, and one without. The appropriate version of this + * sketch must be selected by including or commenting out the + * "#define PIN_SAVING_HARDWARE" statement which is near the top of the code. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + * March 2014 + */ + +#include // may not be needed, but it's probably a good idea to include this + +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; + +#define MAX_DISPLAY_TIME_COUNT 3// no of processing loops between display updates + +// The two versions of the hardware require different logic. +#define PIN_SAVING_HARDWARE + +#ifdef PIN_SAVING_HARDWARE +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. In this version, +// the decimal point has to be treated differently than the other seven segments, so +// a convenient means of accessing this column is provided. +// +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + +#else // PIN_SAVING_HARDWARE + +#define ON HIGH +#define OFF LOW + +const byte noOfSegmentsPerDigit = 8; // includes one for the decimal point +enum digitEnableStates {DIGIT_ENABLED, DIGIT_DISABLED}; + +byte digitSelectorPin[noOfDigitLocations] = {16,10,13,11}; +byte segmentDrivePin[noOfSegmentsPerDigit] = {2,5,12,6,7,9,8,14}; + + +// The final column of segMap[] is for the decimal point status. In this version, +// the decimal point is treated just like all the other segments, so there is +// no need to access this column specifically. +// +byte segMap[noOfPossibleCharacters][noOfSegmentsPerDigit] = { + ON , ON , ON , ON , ON , ON , OFF, OFF, // '0' <- element 0 + OFF, ON , ON , OFF, OFF, OFF, OFF, OFF, // '1' <- element 1 + ON , ON , OFF, ON , ON , OFF, ON , OFF, // '2' <- element 2 + ON , ON , ON , ON , OFF, OFF, ON , OFF, // '3' <- element 3 + OFF, ON , ON , OFF, OFF, ON , ON , OFF, // '4' <- element 4 + ON , OFF, ON , ON , OFF, ON , ON , OFF, // '5' <- element 5 + ON , OFF, ON , ON , ON , ON , ON , OFF, // '6' <- element 6 + ON , ON , ON , OFF, OFF, OFF, OFF, OFF, // '7' <- element 7 + ON , ON , ON , ON , ON , ON , ON , OFF, // '8' <- element 8 + ON , ON , ON , ON , OFF, ON , ON , OFF, // '9' <- element 9 + ON , ON , ON , ON , ON , ON , OFF, ON , // '0.' <- element 10 + OFF, ON , ON , OFF, OFF, OFF, OFF, ON , // '1.' <- element 11 + ON , ON , OFF, ON , ON , OFF, ON , ON , // '2.' <- element 12 + ON , ON , ON , ON , OFF, OFF, ON , ON , // '3.' <- element 13 + OFF, ON , ON , OFF, OFF, ON , ON , ON , // '4.' <- element 14 + ON , OFF, ON , ON , OFF, ON , ON , ON , // '5.' <- element 15 + ON , OFF, ON , ON , ON , ON , ON , ON , // '6.' <- element 16 + ON , ON , ON , OFF, OFF, OFF, OFF, ON , // '7.' <- element 17 + ON , ON , ON , ON , ON , ON , ON , ON , // '8.' <- element 18 + ON , ON , ON , ON , OFF, ON , ON , ON , // '9.' <- element 19 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, // ' ' <- element 20 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, ON // '.' <- element 21 +}; +#endif // PIN_SAVING_HARDWARE + + +byte charsForDisplay[noOfDigitLocations] = {18, 18, 18, 18}; // all segmnents on + + +void setup() +{ +#ifdef PIN_SAVING_HARDWARE + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // control lines for the 74HC4543 7-seg display driver and the DP line + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } + + // control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } +#else + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + pinMode(segmentDrivePin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + pinMode(digitSelectorPin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + digitalWrite(digitSelectorPin[i], DIGIT_DISABLED); } + + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + digitalWrite(segmentDrivePin[i], OFF); } +#endif + + // display the start-up state with all segments on + for (int i = 0; i < 3000; i++) { + delay(1); // to simulate one iteration of loop() in the Mk2 sketch + refreshDisplay(); } + + // clear all segments + for (int i = 0; i < noOfDigitLocations; i++) + { + charsForDisplay[i] = 20; // blank + } + + // display the "all segments off" state for a short while + for (int i = 0; i < 1000; i++) + { + delay(1); // to simulate one iteration of loop() in the Mk2 sketch + refreshDisplay(); + } + + Serial.begin(9600); + Serial.println(); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: segCheck_bothDisplays.ino"); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + + +void loop() +{ + static unsigned long timeOfLastDisplayChange = 0; + static byte digit = 0; // runs from 0 to 3 + static byte value = 0; // runs from 0 to 21 (states 10 to 19 are bypassed) + + unsigned long timeNow = millis(); + if (timeNow - timeOfLastDisplayChange > 500) + { + // the display needs to be updated + timeOfLastDisplayChange = timeNow; + + if (value == 10) + { + value += 10; // to omit the repetition of all digits with the '.' active + } + else + { + if (value >= noOfPossibleCharacters) + { + value = 0; + + charsForDisplay[digit] = 20; // set the digit that's just been exercised to blank. + digit++; + if (digit >= noOfDigitLocations) + { + digit = 0; + } + } + } + + Serial.print (digit); + Serial.print (", "); + Serial.println (value); + charsForDisplay[digit] = value; + + value++; + + } + + delay(1); // to simulate one iteration of loop() in the Mk2 sketch + refreshDisplay(); +} + + + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + // The two versions of the hardware require different logic. + +#ifdef PIN_SAVING_HARDWARE + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } + +#else // PIN_SAVING_HARDWARE + + // This version is more straightforward because the digit-enable lines can be + // used to mask out all of the transitory states, including the Decimal Point. + // The sequence is: + // + // 1. de-activate the digit-enable line that was previously active + // 2. determine the next location which is to be active + // 3. determine the relevant character for the new active location + // 4. set up the segment drivers for the character to be displayed (includes the DP) + // 5. activate the digit-enable line for the new active location + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + displayTime_count = 0; + + // 1. de-activate the location which is currently being displayed + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_DISABLED); + + // 2. determine the next digit location which is to be displayed + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 3. determine the relevant character for the new active location + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 4. set up the segment drivers for the character to be displayed (includes the DP) + for (byte segment = 0; segment < noOfSegmentsPerDigit; segment++) { + byte segmentState = segMap[digitVal][segment]; + digitalWrite( segmentDrivePin[segment], segmentState); } + + // 5. activate the digit-enable line for the new active location + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_ENABLED); + } +#endif // PIN_SAVING_HARDWARE + +} // end of refreshDisplay() + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + diff --git a/docs/routers/mk2pvrouter.co.uk/stocklistWithSources.ods b/docs/routers/mk2pvrouter.co.uk/stocklistWithSources.ods new file mode 100644 index 0000000..f1d381a Binary files /dev/null and b/docs/routers/mk2pvrouter.co.uk/stocklistWithSources.ods differ diff --git a/docs/routers/mk2pvrouter.co.uk/testRig_1.pdf b/docs/routers/mk2pvrouter.co.uk/testRig_1.pdf new file mode 100644 index 0000000..d184696 Binary files /dev/null and b/docs/routers/mk2pvrouter.co.uk/testRig_1.pdf differ diff --git a/docs/routers/mk2pvrouter.com/Mk2_ControleRapide_2Charges_logiqueLowHigh.ino b/docs/routers/mk2pvrouter.com/Mk2_ControleRapide_2Charges_logiqueLowHigh.ino new file mode 100644 index 0000000..740107d --- /dev/null +++ b/docs/routers/mk2pvrouter.com/Mk2_ControleRapide_2Charges_logiqueLowHigh.ino @@ -0,0 +1,1200 @@ +/* Mk2_ControleRapide_2Charges_logiqueLowHigh.ino + * + * (initially released as Mk2_bothDisplays_1 in March 2014) + * This sketch is for diverting suplus PV power to a dump load using a triac or + * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on + * the OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * In this latest version, the pin-allocations have been changed to suit my + * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is + * fed from one of the secondary coils of the transformer. Current is measured + * via Current Transformers at the CT1 and CT1 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. This can be driven in two + * different ways, one with an extra pair of logic chips, and one without. The + * appropriate version of the sketch must be selected by including or commenting + * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code. + * + * September 2014: renamed as Mk2_bothDisplays_2, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - removal of unhelpful comments in the IO pin section; + * - tidier initialisation of display logic in setup(); + * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility); + * + * December 2014: renamed as Mk2_bothDisplays_3, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed) + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances + * + * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed. + * + * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to + * remove a timing uncertainty. + * + * January 2016: updated to Mk2_bothDisplays_3c: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * February 2016: updated to Mk2_bothDisplays_4, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * November 2019: updated to Mk2_fasterControl_1 with these changes: + * - Half way through each mains cycle, a prediction is made of the likely energy level at the + * end of the cycle. That predicted value allows the triac to be switched at the +ve going + * zero-crossing point rather than waiting for a further 10 ms. These changes allow for + * faster switching of the load. + * - The range of the energy bucket has been reduced to one tenth of its former value. This + * allows the unit's operation to commence more rapidly whenever surplus power is available. + * - controlMode is no longer selectable, the unit's operation being effectively hard-coded + * as "Normal" rather than Anti-flicker. + * - Port D3 now supports an indicator which shows when the level in the energy bucket + * reaches either end of its range. While the unit is actively diverting surplus power, + * it is vital that the level in the reduced capacity energy bucket remains within its + * permitted range, hence the addition of this indicator. + * + * February 2020: updated to Mk2_fasterControl_1a with these changes: + * - removal of some redundant code in the logic for determining the next load state. + * + * March 2021: updated to Mk2_fasterControl_2 with these changes: + * - extra filtering added to offset the HPF effect of CT1. This allows the energy state in + * 10 ms time to be predicted with more confidence. Specifically, it is no longer necessary + * to include a 30% boost factor after each change of load state. + * + * June 2021: updated to Mk2_fasterControl_3 with these changes: + * - to reflect the performance of recently manufactured YHDC SCT_013_000 CTs, + * the value of the parameter lpf_gain has been reduced from 12 to 8. + * + * July 2023: updated to Mk2_fasterControl_twoLoads_5 with these changes: + * - the ability to control two loads has been transferred from the latest version of my + * standard multiLoad sketch, Mk2_multiLoad_wired_7a. The faster control algorithm + * has been retained. + * The previous 2-load "faster control" sketch (version 4) has been archived as its + * behaviour was found to be problematic. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define WORKING_RANGE_IN_JOULES 360 // 0.1 Wh, reduced for faster start-up +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +const byte noOfDumploads = 2; // The logic expects a minimum of 2 dumploads, + // for local & remote loads, but neither has to + // be physically present. + +// The two versions of the hardware require different logic. The following line should +// be included if the additional logic chips are present, or excluded if they are +// absent (in which case some wire links need to be fitted) +// +//#define PIN_SAVING_HARDWARE + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum loadStates {LOAD_ON, LOAD_OFF}; // all loads are active low +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +// For this go-faster version, the unit's operation will effectively always be "Normal"; +// there is no "Anti-flicker" option. The controlMode variable has been removed. + +// allocation of digital pins which are not dependent on the display type that is in use +// ************************************************************************************* +const byte physicalLoad_1_pin = 3; // <-- the "mode" port is active-high +const byte physicalLoad_0_pin = 4; // <-- the "trigger" port is active-low + +// allocation of analogue pins which are not dependent on the display type that is in use +// ************************************************************************************** +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + +const byte delayBeforeSerialStarts = 1; // in seconds, to allow Serial window to be opened +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long midPointOfEnergyBucket_long; // used for 'normal' and single-threshold 'AF' logic + +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; +long mainsCyclesPerHour; + +long lowerThreshold_default; +long lowerEnergyThreshold; +long upperThreshold_default; +long upperEnergyThreshold; + +boolean recentTransition = false; +byte postTransitionCount; +#define POST_TRANSITION_MAX_COUNT 3 // <-- allows each transition to take effect +byte activeLoad = 0; + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +volatile int sampleI_grid; +volatile int sampleI_diverted; +volatile int sampleV; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define PERSISTENCE_FOR_POLARITY_CHANGE 1 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "RawSamplesTool_2chan.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + +// for this go-faster sketch, the phaseCal logic has been removed. If required, it can be +// found in most of the standard Mk2_bothDisplay_n versions + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define UPDATE_PERIOD_FOR_DISPLAYED_DATA 50 // mains cycles +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +// The two versions of the hardware require different logic. +#ifdef PIN_SAVING_HARDWARE + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. In this version, +// the decimal point has to be treated differently than the other seven segments, so +// a convenient means of accessing this column is provided. +// +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + +#else // PIN_SAVING_HARDWARE + +#define ON HIGH +#define OFF LOW + +const byte noOfSegmentsPerDigit = 8; // includes one for the decimal point +enum digitEnableStates {DIGIT_ENABLED, DIGIT_DISABLED}; + +byte digitSelectorPin[noOfDigitLocations] = {16,10,13,11}; +byte segmentDrivePin[noOfSegmentsPerDigit] = {2,5,12,6,7,9,8,14}; + +// The final column of segMap[] is for the decimal point status. In this version, +// the decimal point is treated just like all the other segments, so there is +// no need to access this column specifically. +// +byte segMap[noOfPossibleCharacters][noOfSegmentsPerDigit] = { + ON , ON , ON , ON , ON , ON , OFF, OFF, // '0' <- element 0 + OFF, ON , ON , OFF, OFF, OFF, OFF, OFF, // '1' <- element 1 + ON , ON , OFF, ON , ON , OFF, ON , OFF, // '2' <- element 2 + ON , ON , ON , ON , OFF, OFF, ON , OFF, // '3' <- element 3 + OFF, ON , ON , OFF, OFF, ON , ON , OFF, // '4' <- element 4 + ON , OFF, ON , ON , OFF, ON , ON , OFF, // '5' <- element 5 + ON , OFF, ON , ON , ON , ON , ON , OFF, // '6' <- element 6 + ON , ON , ON , OFF, OFF, OFF, OFF, OFF, // '7' <- element 7 + ON , ON , ON , ON , ON , ON , ON , OFF, // '8' <- element 8 + ON , ON , ON , ON , OFF, ON , ON , OFF, // '9' <- element 9 + ON , ON , ON , ON , ON , ON , OFF, ON , // '0.' <- element 10 + OFF, ON , ON , OFF, OFF, OFF, OFF, ON , // '1.' <- element 11 + ON , ON , OFF, ON , ON , OFF, ON , ON , // '2.' <- element 12 + ON , ON , ON , ON , OFF, OFF, ON , ON , // '3.' <- element 13 + OFF, ON , ON , OFF, OFF, ON , ON , ON , // '4.' <- element 14 + ON , OFF, ON , ON , OFF, ON , ON , ON , // '5.' <- element 15 + ON , OFF, ON , ON , ON , ON , ON , ON , // '6.' <- element 16 + ON , ON , ON , OFF, OFF, OFF, OFF, ON , // '7.' <- element 17 + ON , ON , ON , ON , ON , ON , ON , ON , // '8.' <- element 18 + ON , ON , ON , ON , OFF, ON , ON , ON , // '9.' <- element 19 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, // ' ' <- element 20 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, ON // '.' <- element 11 +}; +#endif // PIN_SAVING_HARDWARE + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // energy divertion detection +long requiredExportPerMainsCycle_inIEU; + + +void setup() +{ + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for the local dump-load + pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for an additional load + + for(int i = 0; i< noOfDumploads; i++) + { + logicalLoadState[i] = LOAD_OFF; + physicalLoadState[i] = LOAD_OFF; + } + + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // the local load is active low. + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // additional loads are active high. + + delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_fasterControl_twoLoads_5.ino"); + Serial.println(); + +#ifdef PIN_SAVING_HARDWARE + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } +#else + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + pinMode(segmentDrivePin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + pinMode(digitSelectorPin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + digitalWrite(digitSelectorPin[i], DIGIT_DISABLED); } + + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + digitalWrite(segmentDrivePin[i], OFF); } +#endif + + + + // When using integer maths, calibration values that have supplied in floating point + // form need to be rescaled. + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + midPointOfEnergyBucket_long = capacityOfEnergyBucket_long / 2; + lowerThreshold_default = capacityOfEnergyBucket_long * 0.5; + upperThreshold_default = capacityOfEnergyBucket_long * 0.5; + energyInBucket_long = 0; + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + + Serial.println ("----"); +} + +// An Interrupt Service Routine is now defined in which the ADC is instructed to +// measure each analogue input in sequence. A "data ready" flag is set after each +// voltage conversion has been completed. +// For each set of samples, the two samples for current are taken before the one +// for voltage. This is appropriate because each waveform current is generally slightly +// advanced relative to the waveform for voltage. The data ready flag is cleared +// within loop(). +// This Interrupt Service Routine is for use when the ADC is fixed timer mode. It is +// executed whenever the ADC timer expires. In this mode, the next ADC conversion is +// initiated from within this ISR. +// +void timerIsr(void) +{ + static unsigned char sample_index = 0; + static int sampleI_grid_raw; + static int sampleI_diverted_raw; + + + switch(sample_index) + { + case 0: + sampleV = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current + ADCSRA |= (1< 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + if (beyondStartUpPhase) + { + // a simple routine for checking the performance of this new ISR structure + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / sampleSetsDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step would normally be to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient to just + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long;} + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0;} + + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. + + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) + { + realEnergy_diverted = 0; + } + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + if(timerForDisplayUpdate > UPDATE_PERIOD_FOR_DISPLAYED_DATA) + { // the 4-digit display needs to be refreshed every few mS. For convenience, + // this action is performed every N times around this processing loop. + timerForDisplayUpdate = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); +// Serial.println(energyInBucket_prediction); + } + else + { + timerForDisplayUpdate++; + } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringNegativeHalfOfMainsCycle = 0; + + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringThisMainsCycle = 0; // not yet dealt with for this cycle + sampleCount_forContinuityChecker = 1; // opportunity has been missed for this cycle + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + // (in this go-faster code, the action from here has moved to the negative half of the cycle) + + } // end of processing that is specific to samples where the voltage is positive + + else // the polarity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12); + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + // The average power that has been measured during the first half of this mains cycle can now be used + // to predict the energy state at the end of this mains cycle. That prediction will be used to alter + // the state of the load as necessary. The arming signal for the triac can't be set yet - that must + // wait until the voltage has advanced further beyond the -ve going zero-crossing point. + // + long averagePower = sumP_grid / sampleSetsDuringThisMainsCycle;// for 1st half of this mains cycle only + // + // To avoid repetitive and unnecessary calculations, the increase in energy during each mains cycle is + // deemed to be numerically equal to the average power. The predicted value for the energy state at the + // end of this mains cycle will therefore be the known energy state at its start plus the average power + // as measured. Although the average power has been determined over only half a mains cycle, the correct + // number of contributing sample sets has been used so the result can be expected to be a true measurement + // of average power, not half of it. + // + energyInBucket_prediction = energyInBucket_long + averagePower; // at end of this mains cycle + + + } // end of processing that is specific to the first Vsample in each -ve half cycle + + // check to see whether the trigger device can now be reliably armed + if (sampleSetsDuringNegativeHalfOfMainsCycle == 3) + { + if (beyondStartUpPhase) + { + /* Determining whether any of the loads need to be changed is is a 3-stage process: + * - change the LOGICAL load states as necessary to maintain the energy level + * - update the PHYSICAL load states according to the logical -> physical mapping + * - update the driver lines for each of the loads. + */ + + // Restrictions apply for the period immediately after a load has been switched. + // Here the recentTransition flag is checked and updated as necessary. + if (recentTransition) + { + postTransitionCount++; + if (postTransitionCount >= POST_TRANSITION_MAX_COUNT) + { + recentTransition = false; + } + } + + if (energyInBucket_prediction > midPointOfEnergyBucket_long) + { + // the energy state is in the upper half of the working range + lowerEnergyThreshold = lowerThreshold_default; // reset the "opposite" threshold + if (energyInBucket_prediction > upperEnergyThreshold) + { + // Because the energy level is high, some action may be required + boolean OK_toAddLoad = true; + byte tempLoad = nextLogicalLoadToBeAdded(); + if (tempLoad < noOfDumploads) + { + // a load which is now OFF has been identified for potentially being switched ON + if (recentTransition) + { + // During the post-transition period, any increase in the energy level is noted. + upperEnergyThreshold = energyInBucket_prediction; + + // the energy thresholds must remain within range + if (upperEnergyThreshold > capacityOfEnergyBucket_long) + { + upperEnergyThreshold = capacityOfEnergyBucket_long; + } + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toAddLoad = false; + } + } + + if (OK_toAddLoad) + { + logicalLoadState[tempLoad] = LOAD_ON; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + } + } + } + } + else + { // the energy state is in the lower half of the working range + upperEnergyThreshold = upperThreshold_default; // reset the "opposite" threshold + if (energyInBucket_prediction < lowerEnergyThreshold) + { + // Because the energy level is low, some action may be required + boolean OK_toRemoveLoad = true; + byte tempLoad = nextLogicalLoadToBeRemoved(); + if (tempLoad < noOfDumploads) + { + // a load which is now ON has been identified for potentially being switched OFF + if (recentTransition) + { + // During the post-transition period, any decrease in the energy level is noted. + lowerEnergyThreshold = energyInBucket_prediction; + + // the energy thresholds must remain within range + if (lowerEnergyThreshold < 0) + { + lowerEnergyThreshold = 0; + } + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toRemoveLoad = false; + } + } + + if (OK_toRemoveLoad) + { + logicalLoadState[tempLoad] = LOAD_OFF; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + } + } + } + } + + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + // update each of the physical loads + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger + digitalWrite(physicalLoad_1_pin, !physicalLoadState[1]); // active high for additional load + + // update the Energy Diversion Detector + if (physicalLoadState[0] == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + + // Now that the energy-related decisions have been taken, min and max limits can now + // be applied to the level of the energy bucket. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; } + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; } + } + } + + sampleSetsDuringNegativeHalfOfMainsCycle++; + } // end of processing that is specific to samples where the voltage is negative + + // processing for EVERY set of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + // + // extra filtering to offset the HPF effect of CT1 + long last_lpf_long = lpf_long; + lpf_long = last_lpf_long + alpha *(sampleIminusDC_grid - last_lpf_long); + sampleIminusDC_grid += (lpf_gain * lpf_long); + + // + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + sampleSetsDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count > PERSISTENCE_FOR_POLARITY_CHANGE) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + +byte nextLogicalLoadToBeAdded() +{ + byte retVal = noOfDumploads; + boolean success = false; + for (byte index = 0; index < noOfDumploads && !success; index++) + { + if (logicalLoadState[index] == LOAD_OFF) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +byte nextLogicalLoadToBeRemoved() +{ + byte retVal = noOfDumploads; + boolean success = false; + + // this index counter can't be a 'byte' because the loop would run forever! + // + for (int index = (noOfDumploads -1); index >= 0 && !success; index--) + { + if (logicalLoadState[index] == LOAD_ON) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * The association between the physical and logical loads is 1:1. By default, numerical + * equivalence is maintained, so logical(N) maps to physical(N). If physical load 1 is set + * to have priority, rather than physical load 0, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * Any other mapping relaionships could be configured here. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } + +/* + if (loadPriorityMode == LOAD_1_HAS_PRIORITY) + { + // swap physical loads 0 & 1 if remote load has priority + physicalLoadState[0] = logicalLoadState[1]; + physicalLoadState[1] = logicalLoadState[0]; + } +*/ +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + +// Serial.println(divertedEnergyTotal_Wh); + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +} + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + // The two versions of the hardware require different logic. + +#ifdef PIN_SAVING_HARDWARE + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } + +#else // PIN_SAVING_HARDWARE + + // This version is more straightforward because the digit-enable lines can be + // used to mask out all of the transitory states, including the Decimal Point. + // The sequence is: + // + // 1. de-activate the digit-enable line that was previously active + // 2. determine the next location which is to be active + // 3. determine the relevant character for the new active location + // 4. set up the segment drivers for the character to be displayed (includes the DP) + // 5. activate the digit-enable line for the new active location + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + displayTime_count = 0; + + // 1. de-activate the location which is currently being displayed + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_DISABLED); + + // 2. determine the next digit location which is to be displayed + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 3. determine the relevant character for the new active location + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 4. set up the segment drivers for the character to be displayed (includes the DP) + for (byte segment = 0; segment < noOfSegmentsPerDigit; segment++) { + byte segmentState = segMap[digitVal][segment]; + digitalWrite( segmentDrivePin[segment], segmentState); } + + // 5. activate the digit-enable line for the new active location + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_ENABLED); + } +#endif // PIN_SAVING_HARDWARE + +} // end of refreshDisplay() + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + + diff --git a/docs/routers/mk2pvrouter.com/Notice-Expert-Monophase.pdf b/docs/routers/mk2pvrouter.com/Notice-Expert-Monophase.pdf new file mode 100644 index 0000000..cf46a3b Binary files /dev/null and b/docs/routers/mk2pvrouter.com/Notice-Expert-Monophase.pdf differ diff --git a/docs/routers/mk2pvrouter.com/Notice-Expert-Triphase-avec-relais.pdf b/docs/routers/mk2pvrouter.com/Notice-Expert-Triphase-avec-relais.pdf new file mode 100644 index 0000000..3a57110 Binary files /dev/null and b/docs/routers/mk2pvrouter.com/Notice-Expert-Triphase-avec-relais.pdf differ diff --git a/docs/routers/mk2pvrouter.com/PVRouter_Mono_1_Charge_rap .ino b/docs/routers/mk2pvrouter.com/PVRouter_Mono_1_Charge_rap .ino new file mode 100644 index 0000000..d4e4a8b --- /dev/null +++ b/docs/routers/mk2pvrouter.com/PVRouter_Mono_1_Charge_rap .ino @@ -0,0 +1,1008 @@ +/* Mk2_fasterControl_3.ino + * + * (initially released as Mk2_bothDisplays_1 in March 2014) + * This sketch is for diverting suplus PV power to a dump load using a triac or + * Solid State Relay. It is based on the Mk2i PV Router code that I have posted in on + * the OpenEnergyMonitor forum. The original version, and other related material, + * can be found on my Summary Page at www.openenergymonitor.org/emon/node/1757 + * + * In this latest version, the pin-allocations have been changed to suit my + * PCB-based hardware for the Mk2 PV Router. The integral voltage sensor is + * fed from one of the secondary coils of the transformer. Current is measured + * via Current Transformers at the CT1 and CT1 ports. + * + * CT1 is for 'grid' current, to be measured at the grid supply point. + * CT2 is for the load current, so that diverted energy can be recorded + * + * A persistence-based 4-digit display is supported. This can be driven in two + * different ways, one with an extra pair of logic chips, and one without. The + * appropriate version of the sketch must be selected by including or commenting + * out the "#define PIN_SAVING_HARDWARE" statement near the top of the code. + * + * September 2014: renamed as Mk2_bothDisplays_2, with these changes: + * - cycleCount removed (was not actually used in this sketch, but could have overflowed); + * - removal of unhelpful comments in the IO pin section; + * - tidier initialisation of display logic in setup(); + * - addition of REQUIRED_EXPORT_IN_WATTS logic (useful as a built-in PV simulation facility); + * + * December 2014: renamed as Mk2_bothDisplays_3, with these changes: + * - persistence check added for zero-crossing detection (polarityConfirmed) + * - lowestNoOfSampleSetsPerMainsCycle added, to check for any disturbances + * + * December 2014: renamed as Mk2_bothDisplays_3a, with some typographical errors fixed. + * + * January 2016: renamed as Mk2_bothDisplays_3b, with a minor change in the ISR to + * remove a timing uncertainty. + * + * January 2016: updated to Mk2_bothDisplays_3c: + * The variables to store the ADC results are now declared as "volatile" to remove + * any possibility of incorrect operation due to optimisation by the compiler. + * + * February 2016: updated to Mk2_bothDisplays_4, with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - removal of the unhelpful "triggerNeedsToBeArmed" mechanism + * - tidying of the "confirmPolarity" logic to make its behaviour more clear + * - SWEETZONE_IN_JOULES changed to WORKING_RANGE_IN_JOULES + * - change "triac" to "load" wherever appropriate + * + * November 2019: updated to Mk2_fasterControl_1 with these changes: + * - Half way through each mains cycle, a prediction is made of the likely energy level at the + * end of the cycle. That predicted value allows the triac to be switched at the +ve going + * zero-crossing point rather than waiting for a further 10 ms. These changes allow for + * faster switching of the load. + * - The range of the energy bucket has been reduced to one tenth of its former value. This + * allows the unit's operation to commence more rapidly whenever surplus power is available. + * - controlMode is no longer selectable, the unit's operation being effectively hard-coded + * as "Normal" rather than Anti-flicker. + * - Port D3 now supports an indicator which shows when the level in the energy bucket + * reaches either end of its range. While the unit is actively diverting surplus power, + * it is vital that the level in the reduced capacity energy bucket remains within its + * permitted range, hence the addition of this indicator. + * + * February 2020: updated to Mk2_fasterControl_1a with these changes: + * - removal of some redundant code in the logic for determining the next load state. + * + * March 2021: updated to Mk2_fasterControl_2 with these changes: + * - extra filtering added to offset the HPF effect of CT1. This allows the energy state in + * 10 ms time to be predicted with more confidence. Specifically, it is no longer necessary + * to include a 30% boost factor after each change of load state. + * + * June 2021: updated to Mk2_fasterControl_3 with these changes: + * - to reflect the performance of recently manufactured YHDC SCT_013_000 CTs, + * the value of the parameter lpf_gain has been reduced from 12 to 8. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include +#include + +#define ADC_TIMER_PERIOD 125 // uS (determines the sampling rate / amount of idle time) + +// Physical constants, please do not change! +#define SECONDS_PER_MINUTE 60 +#define MINUTES_PER_HOUR 60 +#define JOULES_PER_WATT_HOUR 3600 // (0.001 kWh = 3600 Joules) + +// Change these values to suit the local mains frequency and supply meter +#define CYCLES_PER_SECOND 50 +#define WORKING_RANGE_IN_JOULES 360 // 0.1 Wh, reduced for faster start-up +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator + +// to prevent the diverted energy total from 'creeping' +#define ANTI_CREEP_LIMIT 5 // in Joules per mains cycle (has no effect when set to 0) +long antiCreepLimit_inIEUperMainsCycle; + +// The two versions of the hardware require different logic. The following line should +// be included if the additional logic chips are present, or excluded if they are +// absent (in which case some wire links need to be fitted) +// +//#define PIN_SAVING_HARDWARE + +// definition of enumerated types +enum polarities {NEGATIVE, POSITIVE}; +enum ledStates {LED_OFF, LED_ON}; // for use at port D3 which is active-high +enum loadStates {LOAD_ON, LOAD_OFF}; // for use at port D4 which is active-low + +// For this go-faster version, the unit's operation will effectively always be "Normal"; +// there is no "Anti-flicker" option. The controlMode variable has been removed. + +// allocation of digital pins which are not dependent on the display type that is in use +// ************************************************************************************* +const byte outOfRangeIndication = 3; // <-- this output port is active-high +const byte outputForTrigger = 4; // <-- this output port is active-low + +// allocation of analogue pins which are not dependent on the display type that is in use +// ************************************************************************************** +const byte voltageSensor = 3; // A3 is for the voltage sensor +const byte currentSensor_diverted = 4; // A4 is for CT2 which measures diverted current +const byte currentSensor_grid = 5; // A5 is for CT1 which measures grid current + +const byte delayBeforeSerialStarts = 1; // in seconds, to allow Serial window to be opened +const byte startUpPeriod = 3; // in seconds, to allow LP filter to settle +const int DCoffset_I = 512; // nominal mid-point value of ADC @ x1 scale + +// General global variables that are used in multiple blocks so cannot be static. +// For integer maths, many variables need to be 'long' +// +boolean beyondStartUpPhase = false; // start-up delay, allows things to settle +long energyInBucket_long; // in Integer Energy Units +long capacityOfEnergyBucket_long; // depends on powerCal, frequency & the 'sweetzone' size. +long singleEnergyThreshold_long; // for go-faster use + +long DCoffset_V_long; // <--- for LPF +long DCoffset_V_min; // <--- for LPF +long DCoffset_V_max; // <--- for LPF +long divertedEnergyRecent_IEU = 0; // Hi-res accumulator of limited range +unsigned int divertedEnergyTotal_Wh = 0; // WattHour register of 63K range +long IEU_per_Wh; // depends on powerCal, frequency & the 'sweetzone' size. + +unsigned long displayShutdown_inMainsCycles; +unsigned long absenceOfDivertedEnergyCount = 0; +long mainsCyclesPerHour; + +// for interaction between the main processor and the ISRs +volatile boolean dataReady = false; +volatile int sampleI_grid; +volatile int sampleI_diverted; +volatile int sampleV; + +// For an enhanced polarity detection mechanism, which includes a persistence check +#define PERSISTENCE_FOR_POLARITY_CHANGE 1 // sample sets +enum polarities polarityOfMostRecentVsample; +enum polarities polarityConfirmed; +enum polarities polarityConfirmedOfLastSampleV; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int sampleCount_forContinuityChecker; +int sampleSetsDuringThisMainsCycle; +int lowestNoOfSampleSetsPerMainsCycle; + +// Calibration values +//------------------- +// Two calibration values are used: powerCal and phaseCal. +// A full explanation of each of these values now follows: +// +// powerCal is a floating point variable which is used for converting the +// product of voltage and current samples into Watts. +// +// The correct value of powerCal is dependent on the hardware that is +// in use. For best resolution, the hardware should be configured so that the +// voltage and current waveforms each span most of the ADC's usable range. For +// many systems, the maximum power that will need to be measured is around 3kW. +// +// My sketch "RawSamplesTool_2chan.ino" provides a one-shot visual display of the +// voltage and current waveforms. This provides an easy way for the user to be +// confident that their system has been set up correctly for the power levels +// that are to be measured. +// +// The ADC has an input range of 0-5V and an output range of 0-1023 levels. +// The purpose of each input sensor is to convert the measured parameter into a +// low-voltage signal which fits nicely within the ADC's input range. +// +// In the case of 240V mains voltage, the numerical value of the input signal +// in Volts is likely to be fairly similar to the output signal in ADC levels. +// 240V AC has a peak-to-peak amplitude of 679V, which is not far from the ideal +// output range. Stated more formally, the conversion rate of the overall system +// for measuring VOLTAGE is likely to be around 1 ADC-step per Volt (RMS). +// +// In the case of AC current, however, the situation is very different. At +// mains voltage, a power of 3kW corresponds to an RMS current of 12.5A which +// has a peak-to-peak range of 35A. This is smaller than the output signal by +// around a factor of twenty. The conversion rate of the overall system for +// measuring CURRENT is therefore likely to be around 20 ADC-steps per Amp. +// +// When calculating "real power", which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal_grid = 0.0435; // for CT1 +const float powerCal_diverted = 0.0435; // for CT2 + +// for this go-faster sketch, the phaseCal logic has been removed. If required, it can be +// found in most of the standard Mk2_bothDisplay_n versions + +// Various settings for the 4-digit display, which needs to be refreshed every few mS +const byte noOfDigitLocations = 4; +const byte noOfPossibleCharacters = 22; +#define MAX_DISPLAY_TIME_COUNT 10// no of processing loops between display updates +#define UPDATE_PERIOD_FOR_DISPLAYED_DATA 50 // mains cycles +#define DISPLAY_SHUTDOWN_IN_HOURS 8 // auto-reset after this period of inactivity +// #define DISPLAY_SHUTDOWN_IN_HOURS 0.01 // for testing that the display clears after 36 seconds + +// The two versions of the hardware require different logic. +#ifdef PIN_SAVING_HARDWARE + +#define DRIVER_CHIP_DISABLED HIGH +#define DRIVER_CHIP_ENABLED LOW + +// the primary segments are controlled by a pair of logic chips +const byte noOfDigitSelectionLines = 4; // <- for the 74HC4543 7-segment display driver +const byte noOfDigitLocationLines = 2; // <- for the 74HC138 2->4 line demultiplexer + +byte enableDisableLine = 5; // <- affects the primary 7 segments only (not the DP) +byte decimalPointLine = 14; // <- this line has to be individually controlled. + +byte digitLocationLine[noOfDigitLocationLines] = {16,15}; +byte digitSelectionLine[noOfDigitSelectionLines] = {7,9,8,6}; + +// The final column of digitValueMap[] is for the decimal point status. In this version, +// the decimal point has to be treated differently than the other seven segments, so +// a convenient means of accessing this column is provided. +// +byte digitValueMap[noOfPossibleCharacters][noOfDigitSelectionLines +1] = { + LOW , LOW , LOW , LOW , LOW , // '0' <- element 0 + LOW , LOW , LOW , HIGH, LOW , // '1' <- element 1 + LOW , LOW , HIGH, LOW , LOW , // '2' <- element 2 + LOW , LOW , HIGH, HIGH, LOW , // '3' <- element 3 + LOW , HIGH, LOW , LOW , LOW , // '4' <- element 4 + LOW , HIGH, LOW , HIGH, LOW , // '5' <- element 5 + LOW , HIGH, HIGH, LOW , LOW , // '6' <- element 6 + LOW , HIGH, HIGH, HIGH, LOW , // '7' <- element 7 + HIGH, LOW , LOW , LOW , LOW , // '8' <- element 8 + HIGH, LOW , LOW , HIGH, LOW , // '9' <- element 9 + LOW , LOW , LOW , LOW , HIGH, // '0.' <- element 10 + LOW , LOW , LOW , HIGH, HIGH, // '1.' <- element 11 + LOW , LOW , HIGH, LOW , HIGH, // '2.' <- element 12 + LOW , LOW , HIGH, HIGH, HIGH, // '3.' <- element 13 + LOW , HIGH, LOW , LOW , HIGH, // '4.' <- element 14 + LOW , HIGH, LOW , HIGH, HIGH, // '5.' <- element 15 + LOW , HIGH, HIGH, LOW , HIGH, // '6.' <- element 16 + LOW , HIGH, HIGH, HIGH, HIGH, // '7.' <- element 17 + HIGH, LOW , LOW , LOW , HIGH, // '8.' <- element 18 + HIGH, LOW , LOW , HIGH, HIGH, // '9.' <- element 19 + HIGH, HIGH, HIGH, HIGH, LOW , // ' ' <- element 20 + HIGH, HIGH, HIGH, HIGH, HIGH // '.' <- element 21 +}; + +// a tidy means of identifying the DP status data when accessing the above table +const byte DPstatus_columnID = noOfDigitSelectionLines; + +byte digitLocationMap[noOfDigitLocations][noOfDigitLocationLines] = { + LOW , LOW , // Digit 1 + LOW , HIGH, // Digit 2 + HIGH, LOW , // Digit 3 + HIGH, HIGH, // Digit 4 +}; + +#else // PIN_SAVING_HARDWARE + +#define ON HIGH +#define OFF LOW + +const byte noOfSegmentsPerDigit = 8; // includes one for the decimal point +enum digitEnableStates {DIGIT_ENABLED, DIGIT_DISABLED}; + +byte digitSelectorPin[noOfDigitLocations] = {16,10,13,11}; +byte segmentDrivePin[noOfSegmentsPerDigit] = {2,5,12,6,7,9,8,14}; + +// The final column of segMap[] is for the decimal point status. In this version, +// the decimal point is treated just like all the other segments, so there is +// no need to access this column specifically. +// +byte segMap[noOfPossibleCharacters][noOfSegmentsPerDigit] = { + ON , ON , ON , ON , ON , ON , OFF, OFF, // '0' <- element 0 + OFF, ON , ON , OFF, OFF, OFF, OFF, OFF, // '1' <- element 1 + ON , ON , OFF, ON , ON , OFF, ON , OFF, // '2' <- element 2 + ON , ON , ON , ON , OFF, OFF, ON , OFF, // '3' <- element 3 + OFF, ON , ON , OFF, OFF, ON , ON , OFF, // '4' <- element 4 + ON , OFF, ON , ON , OFF, ON , ON , OFF, // '5' <- element 5 + ON , OFF, ON , ON , ON , ON , ON , OFF, // '6' <- element 6 + ON , ON , ON , OFF, OFF, OFF, OFF, OFF, // '7' <- element 7 + ON , ON , ON , ON , ON , ON , ON , OFF, // '8' <- element 8 + ON , ON , ON , ON , OFF, ON , ON , OFF, // '9' <- element 9 + ON , ON , ON , ON , ON , ON , OFF, ON , // '0.' <- element 10 + OFF, ON , ON , OFF, OFF, OFF, OFF, ON , // '1.' <- element 11 + ON , ON , OFF, ON , ON , OFF, ON , ON , // '2.' <- element 12 + ON , ON , ON , ON , OFF, OFF, ON , ON , // '3.' <- element 13 + OFF, ON , ON , OFF, OFF, ON , ON , ON , // '4.' <- element 14 + ON , OFF, ON , ON , OFF, ON , ON , ON , // '5.' <- element 15 + ON , OFF, ON , ON , ON , ON , ON , ON , // '6.' <- element 16 + ON , ON , ON , OFF, OFF, OFF, OFF, ON , // '7.' <- element 17 + ON , ON , ON , ON , ON , ON , ON , ON , // '8.' <- element 18 + ON , ON , ON , ON , OFF, ON , ON , ON , // '9.' <- element 19 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, OFF, // ' ' <- element 20 + OFF, OFF, OFF, OFF, OFF, OFF, OFF, ON // '.' <- element 11 +}; +#endif // PIN_SAVING_HARDWARE + +byte charsForDisplay[noOfDigitLocations] = {20,20,20,20}; // all blank + +boolean EDD_isActive = false; // energy divertion detection +long requiredExportPerMainsCycle_inIEU; + + +void setup() +{ + pinMode(outOfRangeIndication, OUTPUT); + digitalWrite (outOfRangeIndication, LED_OFF); + + pinMode(outputForTrigger, OUTPUT); + digitalWrite (outputForTrigger, LOAD_OFF); + + delay(delayBeforeSerialStarts * 1000); // allow time to open Serial monitor + + Serial.begin(9600); + Serial.println(); + Serial.println("-------------------------------------"); + Serial.println("Sketch ID: Mk2_fasterControl_3.ino"); + Serial.println(); + +#ifdef PIN_SAVING_HARDWARE + // configure the IO drivers for the 4-digit display + // + // the Decimal Point line is driven directly from the processor + pinMode(decimalPointLine, OUTPUT); // the 'decimal point' line + + // set up the control lines for the 74HC4543 7-seg display driver + for (int i = 0; i < noOfDigitSelectionLines; i++) { + pinMode(digitSelectionLine[i], OUTPUT); } + + // an enable line is required for the 74HC4543 7-seg display driver + pinMode(enableDisableLine, OUTPUT); // for the 74HC4543 7-seg display driver + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // set up the control lines for the 74HC138 2->4 demux + for (int i = 0; i < noOfDigitLocationLines; i++) { + pinMode(digitLocationLine[i], OUTPUT); } +#else + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + pinMode(segmentDrivePin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + pinMode(digitSelectorPin[i], OUTPUT); } + + for (int i = 0; i < noOfDigitLocations; i++) { + digitalWrite(digitSelectorPin[i], DIGIT_DISABLED); } + + for (int i = 0; i < noOfSegmentsPerDigit; i++) { + digitalWrite(segmentDrivePin[i], OFF); } +#endif + + + + // When using integer maths, calibration values that have supplied in floating point + // form need to be rescaled. + + // When using integer maths, the SIZE of the ENERGY BUCKET is altered to match the + // scaling of the energy detection mechanism that is in use. This avoids the need + // to re-scale every energy contribution, thus saving processing time. This process + // is described in more detail in the function, allGeneralProcessing(), just before + // the energy bucket is updated at the start of each new cycle of the mains. + // + // An electricity meter has a small range over which energy can ebb and flow without + // penalty. This has been termed its "sweet-zone". For optimal performance, the energy + // bucket of a PV Router should match this value. The sweet-zone value is therefore + // included in the calculation below. + // + // For the flow of energy at the 'grid' connection point (CT1) + capacityOfEnergyBucket_long = + (long)WORKING_RANGE_IN_JOULES * CYCLES_PER_SECOND * (1/powerCal_grid); + energyInBucket_long = 0; + + singleEnergyThreshold_long = capacityOfEnergyBucket_long * 0.5; + + // For recording the accumulated amount of diverted energy data (using CT2), a similar + // calibration mechanism is required. Rather than a bucket with a fixed capacity, the + // accumulator for diverted energy just needs to be scaled correctly. As soon as its + // value exceeds 1 Wh, an associated WattHour register is incremented, and the + // accumulator's value is decremented accordingly. The calculation below is to determine + // the scaling for this accumulator. + + IEU_per_Wh = + (long)JOULES_PER_WATT_HOUR * CYCLES_PER_SECOND * (1/powerCal_diverted); + + // to avoid the diverted energy accumulator 'creeping' when the load is not active + antiCreepLimit_inIEUperMainsCycle = (float)ANTI_CREEP_LIMIT * (1/powerCal_grid); + + mainsCyclesPerHour = (long)CYCLES_PER_SECOND * + SECONDS_PER_MINUTE * MINUTES_PER_HOUR; + + displayShutdown_inMainsCycles = DISPLAY_SHUTDOWN_IN_HOURS * mainsCyclesPerHour; + + requiredExportPerMainsCycle_inIEU = (long)REQUIRED_EXPORT_IN_WATTS * (1/powerCal_grid); + + + // Define operating limits for the LP filter which identifies DC offset in the voltage + // sample stream. By limiting the output range, the filter always should start up + // correctly. + DCoffset_V_long = 512L * 256; // nominal mid-point value of ADC @ x256 scale + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + + Serial.print ("ADC mode: "); + Serial.print (ADC_TIMER_PERIOD); + Serial.println ( " uS fixed timer"); + + // Set up the ADC to be triggered by a hardware timer of fixed duration + ADCSRA = (1<>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on + + Serial.println ("----"); +} + +// An Interrupt Service Routine is now defined in which the ADC is instructed to +// measure each analogue input in sequence. A "data ready" flag is set after each +// voltage conversion has been completed. +// For each set of samples, the two samples for current are taken before the one +// for voltage. This is appropriate because each waveform current is generally slightly +// advanced relative to the waveform for voltage. The data ready flag is cleared +// within loop(). +// This Interrupt Service Routine is for use when the ADC is fixed timer mode. It is +// executed whenever the ADC timer expires. In this mode, the next ADC conversion is +// initiated from within this ISR. +// +void timerIsr(void) +{ + static unsigned char sample_index = 0; + static int sampleI_grid_raw; + static int sampleI_diverted_raw; + + + switch(sample_index) + { + case 0: + sampleV = ADC; // store the ADC value (this one is for Voltage) + ADMUX = 0x40 + currentSensor_diverted; // set up the next conversion, which is for Diverted Current + ADCSRA |= (1< 0) { + polarityOfMostRecentVsample = POSITIVE; } + else { + polarityOfMostRecentVsample = NEGATIVE; } + confirmPolarity(); + + if (polarityConfirmed == POSITIVE) + { + if (polarityConfirmedOfLastSampleV != POSITIVE) + { + // This is the start of a new +ve half cycle (just after the zero-crossing point) + if (beyondStartUpPhase) + { + // a simple routine for checking the performance of this new ISR structure + if (sampleSetsDuringThisMainsCycle < lowestNoOfSampleSetsPerMainsCycle) { + lowestNoOfSampleSetsPerMainsCycle = sampleSetsDuringThisMainsCycle; } + + // Calculate the real power and energy during the last whole mains cycle. + // + // sumP contains the sum of many individual calculations of instantaneous power. In + // order to obtain the average power during the relevant period, sumP must first be + // divided by the number of samples that have contributed to its value. + // + // The next stage would normally be to apply a calibration factor so that real power + // can be expressed in Watts. That's fine for floating point maths, but it's not such + // a good idea when integer maths is being used. To keep the numbers large, and also + // to save time, calibration of power is omitted at this stage. Real Power (stored as + // a 'long') is therefore (1/powerCal) times larger than the actual power in Watts. + // + long realPower_grid = sumP_grid / sampleSetsDuringThisMainsCycle; // proportional to Watts + long realPower_diverted = sumP_diverted / sampleSetsDuringThisMainsCycle; // proportional to Watts + + realPower_grid -= requiredExportPerMainsCycle_inIEU; // <- useful for PV simulation + + // Next, the energy content of this power rating needs to be determined. Energy is + // power multiplied by time, so the next step would normally be to multiply the measured + // value of power by the time over which it was measured. + // Instanstaneous power is calculated once every mains cycle. When integer maths is + // being used, a repetitive power-to-energy conversion seems an unnecessary workload. + // As all sampling periods are of similar duration, it is more efficient to just + // add all of the power samples together, and note that their sum is actually + // CYCLES_PER_SECOND greater than it would otherwise be. + // Although the numerical value itself does not change, I thought that a new name + // may be helpful so as to minimise confusion. + // The 'energy' variable below is CYCLES_PER_SECOND * (1/powerCal) times larger than + // the actual energy in Joules. + // + long realEnergy_grid = realPower_grid; + long realEnergy_diverted = realPower_diverted; + + // Energy contributions from the grid connection point (CT1) are summed in an + // accumulator which is known as the energy bucket. The purpose of the energy bucket + // is to mimic the operation of the supply meter. The range over which energy can + // pass to and fro without loss or charge to the user is known as its 'sweet-zone'. + // The capacity of the energy bucket is set to this same value within setup(). + // + // The latest contribution can now be added to this energy bucket + energyInBucket_long += realEnergy_grid; + + // Apply max and min limits to bucket's level. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + bool endOfRangeEncountered = false; + if (energyInBucket_long > capacityOfEnergyBucket_long) { + energyInBucket_long = capacityOfEnergyBucket_long; + endOfRangeEncountered = true;} + else + if (energyInBucket_long < 0) { + energyInBucket_long = 0; + endOfRangeEncountered = true;} + + if (endOfRangeEncountered) { + digitalWrite (outOfRangeIndication , LED_ON); } + else { + digitalWrite (outOfRangeIndication , LED_OFF); } + + if (EDD_isActive) // Energy Diversion Display + { + // For diverted energy, the latest contribution needs to be added to an + // accumulator which operates with maximum precision. + + if (realEnergy_diverted < antiCreepLimit_inIEUperMainsCycle) + { + realEnergy_diverted = 0; + } + divertedEnergyRecent_IEU += realEnergy_diverted; + + // Whole kWhours are then recorded separately + if (divertedEnergyRecent_IEU > IEU_per_Wh) + { + divertedEnergyRecent_IEU -= IEU_per_Wh; + divertedEnergyTotal_Wh++; + } + } + + if(timerForDisplayUpdate > UPDATE_PERIOD_FOR_DISPLAYED_DATA) + { // the 4-digit display needs to be refreshed every few mS. For convenience, + // this action is performed every N times around this processing loop. + timerForDisplayUpdate = 0; + + // After a pre-defined period of inactivity, the 4-digit display needs to + // close down in readiness for the next's day's data. + // + if (absenceOfDivertedEnergyCount > displayShutdown_inMainsCycles) + { + // clear the accumulators for diverted energy + divertedEnergyTotal_Wh = 0; + divertedEnergyRecent_IEU = 0; + EDD_isActive = false; // energy diversion detector is now inactive + } + + configureValueForDisplay(); + } + else + { + timerForDisplayUpdate++; + } + + // continuity checker + sampleCount_forContinuityChecker++; + if (sampleCount_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + sampleCount_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + + // clear the per-cycle accumulators for use in this new mains cycle. + sampleSetsDuringThisMainsCycle = 0; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringNegativeHalfOfMainsCycle = 0; + + } + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (delayBeforeSerialStarts + startUpPeriod) * 1000) + { + beyondStartUpPhase = true; + sumP_grid = 0; + sumP_diverted = 0; + sampleSetsDuringThisMainsCycle = 0; // not yet dealt with for this cycle + sampleCount_forContinuityChecker = 1; // opportunity has been missed for this cycle + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + // (in this go-faster code, the action from here has moved to the negative half of the cycle) + + } // end of processing that is specific to samples where the voltage is positive + + else // the polatity of this sample is negative + { + if (polarityConfirmedOfLastSampleV != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // which is a convenient point to update the Low Pass Filter for DC-offset removal + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + long previousOffset = DCoffset_V_long; + DCoffset_V_long = previousOffset + (cumVdeltasThisCycle_long>>12); + cumVdeltasThisCycle_long = 0; + + // To ensure that the LPF will always start up correctly when 240V AC is available, its + // output value needs to be prevented from drifting beyond the likely range of the + // voltage signal. This avoids the need to use a HPF as was done for initial Mk2 builds. + // + if (DCoffset_V_long < DCoffset_V_min) { + DCoffset_V_long = DCoffset_V_min; } + else + if (DCoffset_V_long > DCoffset_V_max) { + DCoffset_V_long = DCoffset_V_max; } + + // The average power that has been measured during the first half of this mains cycle can now be used + // to predict the energy state at the end of this mains cycle. That prediction will be used to alter + // the state of the load as necessary. The arming signal for the triac can't be set yet - that must + // wait until the voltage has advanced further beyond the -ve going zero-crossing point. + // + long averagePower = sumP_grid / sampleSetsDuringThisMainsCycle;// for 1st half of this mains cycle only + // + // To avoid repetitive and unnecessary calculations, the increase in energy during each mains cycle is + // deemed to be numerically equal to the average power. The predicted value for the energy state at the + // end of this mains cycle will therefore be the known energy state at its start plus the average power + // as measured. Although the average power has been determined over only half a mains cycle, the correct + // number of contributing sample sets has been used so the result can be expected to be a true measurement + // of average power, not half of it. + // + energyInBucket_prediction = energyInBucket_long + averagePower; // at end of this mains cycle + + + } // end of processing that is specific to the first Vsample in each -ve half cycle + + // check to see whether the trigger device can now be reliably armed + if (sampleSetsDuringNegativeHalfOfMainsCycle == 3) + { + if (beyondStartUpPhase) + { + enum loadStates prevStateOfLoad = nextStateOfLoad; + if (energyInBucket_prediction < singleEnergyThreshold_long) { + nextStateOfLoad = LOAD_OFF; } + else + nextStateOfLoad = LOAD_ON; + + // set the Arduino's output pin accordingly, and clear the flag + digitalWrite(outputForTrigger, nextStateOfLoad); + + // update the Energy Diversion Detector + if (nextStateOfLoad == LOAD_ON) { + absenceOfDivertedEnergyCount = 0; + EDD_isActive = true; } + else { + absenceOfDivertedEnergyCount++; } + } + } + + sampleSetsDuringNegativeHalfOfMainsCycle++; + } // end of processing that is specific to samples where the voltage is negative + + // processing for EVERY set of samples + // + // First, deal with the power at the grid connection point (as measured via CT1) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_grid = ((long)(sampleI_grid-DCoffset_I))<<8; + // + // extra filtering to offset the HPF effect of CT1 + long last_lpf_long = lpf_long; + lpf_long = last_lpf_long + alpha *(sampleIminusDC_grid - last_lpf_long); + sampleIminusDC_grid += (lpf_gain * lpf_long); + + // + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = sampleVminusDC_long>>2; // reduce to 16-bits (now x64, or 2^6) + long filtI_div4 = sampleIminusDC_grid>>2; // reduce to 16-bits (now x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_grid +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + // Now deal with the diverted power (as measured via CT2) + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_diverted = ((long)(sampleI_diverted-DCoffset_I))<<8; + + // calculate the "real power" in this sample pair and add to the accumulated sum + filtI_div4 = sampleIminusDC_diverted>>2; // reduce to 16-bits (now x64, or 2^6) + instP = filtV_div4 * filtI_div4; // 32-bits (now x4096, or 2^12) + instP = instP>>12; // scaling is now x1, as for Mk2 (V_ADC x I_ADC) + sumP_diverted +=instP; // cumulative power, scaling as for Mk2 (V_ADC x I_ADC) + + sampleSetsDuringThisMainsCycle++; + + // store items for use during next loop + cumVdeltasThisCycle_long += sampleVminusDC_long; // for use with LP filter + polarityConfirmedOfLastSampleV = polarityConfirmed; // for identification of half cycle boundaries + + refreshDisplay(); +} +// ----- end of main Mk2i code ----- + +void confirmPolarity() +{ + /* This routine prevents a zero-crossing point from being declared until + * a certain number of consecutive samples in the 'other' half of the + * waveform have been encountered. + */ + static byte count = 0; + if (polarityOfMostRecentVsample != polarityConfirmedOfLastSampleV) { + count++; } + else { + count = 0; } + + if (count > PERSISTENCE_FOR_POLARITY_CHANGE) + { + count = 0; + polarityConfirmed = polarityOfMostRecentVsample; + } +} + +// called infrequently, to update the characters to be displayed +void configureValueForDisplay() +{ + static byte locationOfDot = 0; + +// Serial.println(divertedEnergyTotal_Wh); + + if (EDD_isActive) + { + unsigned int val = divertedEnergyTotal_Wh; + boolean energyValueExceeds10kWh; + + if (val < 10000) { + // no need to re-scale (display to 3 DPs) + energyValueExceeds10kWh = false; } + else { + // re-scale is needed (display to 2 DPs) + energyValueExceeds10kWh = true; + val = val/10; } + + byte thisDigit = val / 1000; + charsForDisplay[0] = thisDigit; + val -= 1000 * thisDigit; + + thisDigit = val / 100; + charsForDisplay[1] = thisDigit; + val -= 100 * thisDigit; + + thisDigit = val / 10; + charsForDisplay[2] = thisDigit; + val -= 10 * thisDigit; + + charsForDisplay[3] = val; + + // assign the decimal point location + if (energyValueExceeds10kWh) { + charsForDisplay[1] += 10; } // dec point after 2nd digit + else { + charsForDisplay[0] += 10; } // dec point after 1st digit + } + else + { + // "walking dots" display + charsForDisplay[locationOfDot] = 20; // blank + + locationOfDot++; + if (locationOfDot >= noOfDigitLocations) { + locationOfDot = 0; } + + charsForDisplay[locationOfDot] = 21; // dot + } +} + +void refreshDisplay() +{ + // This routine keeps track of which digit is being displayed and checks when its + // display time has expired. It then makes the necessary adjustments for displaying + // the next digit. + // The two versions of the hardware require different logic. + +#ifdef PIN_SAVING_HARDWARE + // With this version of the hardware, care must be taken that all transitory states + // are masked out. Note that the enableDisableLine only masks the seven primary + // segments, not the Decimal Point line which must therefore be treated separately. + // The sequence is: + // + // 1. set the decimal point line to 'off' + // 2. disable the 7-segment driver chip + // 3. determine the next location which is to be active + // 4. set up the location lines for the new active location + // 5. determine the relevant character for the new active location + // 6. configure the driver chip for the new character to be displayed + // 7. set up decimal point line for the new active location + // 8. enable the 7-segment driver chip + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + byte lineState; + + displayTime_count = 0; + + // 1. disable the Decimal Point driver line; + digitalWrite( decimalPointLine, LOW); + + // 2. disable the driver chip while changes are taking place + digitalWrite( enableDisableLine, DRIVER_CHIP_DISABLED); + + // 3. determine the next digit location to be active + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 4. set up the digit location drivers for the new active location + for (byte line = 0; line < noOfDigitLocationLines; line++) { + lineState = digitLocationMap[digitLocationThatIsActive][line]; + digitalWrite( digitLocationLine[line], lineState); } + + // 5. determine the character to be displayed at this new location + // (which includes the decimal point information) + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 6. configure the 7-segment driver for the character to be displayed + for (byte line = 0; line < noOfDigitSelectionLines; line++) { + lineState = digitValueMap[digitVal][line]; + digitalWrite( digitSelectionLine[line], lineState); } + + // 7. set up the Decimal Point driver line; + digitalWrite( decimalPointLine, digitValueMap[digitVal][DPstatus_columnID]); + + // 8. enable the 7-segment driver chip + digitalWrite( enableDisableLine, DRIVER_CHIP_ENABLED); + } + +#else // PIN_SAVING_HARDWARE + + // This version is more straightforward because the digit-enable lines can be + // used to mask out all of the transitory states, including the Decimal Point. + // The sequence is: + // + // 1. de-activate the digit-enable line that was previously active + // 2. determine the next location which is to be active + // 3. determine the relevant character for the new active location + // 4. set up the segment drivers for the character to be displayed (includes the DP) + // 5. activate the digit-enable line for the new active location + + static byte displayTime_count = 0; + static byte digitLocationThatIsActive = 0; + + displayTime_count++; + + if (displayTime_count > MAX_DISPLAY_TIME_COUNT) + { + displayTime_count = 0; + + // 1. de-activate the location which is currently being displayed + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_DISABLED); + + // 2. determine the next digit location which is to be displayed + digitLocationThatIsActive++; + if (digitLocationThatIsActive >= noOfDigitLocations) { + digitLocationThatIsActive = 0; } + + // 3. determine the relevant character for the new active location + byte digitVal = charsForDisplay[digitLocationThatIsActive]; + + // 4. set up the segment drivers for the character to be displayed (includes the DP) + for (byte segment = 0; segment < noOfSegmentsPerDigit; segment++) { + byte segmentState = segMap[digitVal][segment]; + digitalWrite( segmentDrivePin[segment], segmentState); } + + // 5. activate the digit-enable line for the new active location + digitalWrite(digitSelectorPin[digitLocationThatIsActive], DIGIT_ENABLED); + } +#endif // PIN_SAVING_HARDWARE + +} // end of refreshDisplay() + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + + + diff --git a/docs/routers/mk2pvrouter.com/Programmation-PVrouter.pdf b/docs/routers/mk2pvrouter.com/Programmation-PVrouter.pdf new file mode 100644 index 0000000..8922a8c Binary files /dev/null and b/docs/routers/mk2pvrouter.com/Programmation-PVrouter.pdf differ diff --git "a/docs/routers/mk2pvrouter.com/Triphas\303\251.ino" "b/docs/routers/mk2pvrouter.com/Triphas\303\251.ino" new file mode 100644 index 0000000..82c9473 --- /dev/null +++ "b/docs/routers/mk2pvrouter.com/Triphas\303\251.ino" @@ -0,0 +1,963 @@ +/* Mk2_3phase_RFdatalog_4.ino + * + * Issue 1 was released in January 2015. + * + * This sketch provides continuous monitoring of real power on three phases. + * Surplus power is diverted to multiple loads in sequential order. A suitable + * output-stage is required for each load; this can be either triac-based, or a + * Solid State Relay. + * + * Datalogging of real power and Vrms is provided for each phase. + * The presence or absence of the RFM12B needs to be set at compile time + * + * January 2016, renamed as Mk2_3phase_RFdatalog_2 with these changes: + * - Improved control of multiple loads has been imported from the + * equivalent 1-phase sketch, Mk2_multiLoad_wired_6.ino + * - the ISR has been upgraded to fix a possible timing anomaly + * - variables to store ADC samples are now declared as "volatile" + * - for RF69 RF module is now supported + * - a performance check has been added with the result being sent to the Serial port + * - control signals for loads are now active-high to suit the latest 3-phase PCB + * + * February 2016, renamed as Mk2_3phase_RFdatalog_3 with these changes: + * - improvements to the start-up logic. The start of normal operation is now + * synchronised with the start of a new mains cycle. + * - reduce the amount of feedback in the Low Pass Filter for removing the DC content + * from the Vsample stream. This resolves an anomaly which has been present since + * the start of this project. Although the amount of feedback has previously been + * excessive, this anomaly has had minimal effect on the system's overall behaviour. + * - The reported power at each of the phases has been inverted. These values are now in + * line with the Open Energy Monitor convention, whereby import is positive and + * export is negative. + * + * February 2020: updated to Mk2_3phase_RFdatalog_3a with these changes: + * - removal of some redundant code in the logic for determining the next load state. + * + * July 2022: updated to Mk2_3phase_RFdatalog_4, with this change: + * - the datalogging accumulator for Vsquared has been rescaled to 1/16 of its previous value + * to avoid the risk of overflowing during a 20-second datalogging period. + * + * Robin Emley + * www.Mk2PVrouter.co.uk + */ + +#include // may not be needed, but it's probably a good idea to include this + +//#define RF_PRESENT // <- this line should be commented out if the RFM12B module is not present + +#ifdef RF_PRESENT +//#define RF69_COMPAT 0 // for the RFM12B +#define RF69_COMPAT 1 // for the RF69 +#include +#endif + +// In this sketch, the ADC is free-running with a cycle time of ~104uS. + +// WORKLOAD_CHECK is available for determining how much spare processing time there +// is. To activate this mode, the #define line below should be included: +//#define WORKLOAD_CHECK + +#define CYCLES_PER_SECOND 50 +//#define JOULES_PER_WATT_HOUR 3600 // may be needed for datalogging +#define WORKING_ZONE_IN_JOULES 3600 +#define REQUIRED_EXPORT_IN_WATTS 0 // when set to a negative value, this acts as a PV generator +#define NO_OF_PHASES 3 +#define DATALOG_PERIOD_IN_SECONDS 10 + +const byte noOfDumploads = 3; + +enum polarities {NEGATIVE, POSITIVE}; +enum outputModes {ANTI_FLICKER, NORMAL}; +enum loadPriorityModes {LOAD_1_HAS_PRIORITY, LOAD_0_HAS_PRIORITY}; + +// enum loadStates {LOAD_ON, LOAD_OFF}; // for use if loads are active low (original PCB) +enum loadStates {LOAD_OFF, LOAD_ON}; // for use if loads are active high (Rev 2 PCB) +enum loadStates logicalLoadState[noOfDumploads]; +enum loadStates physicalLoadState[noOfDumploads]; + +// For this multi-load version, the same mechanism has been retained but the +// output mode is hard-coded as below: +enum outputModes outputMode = NORMAL; + +// In this multi-load version, the external switch is re-used to determine the load priority +enum loadPriorityModes loadPriorityMode = LOAD_0_HAS_PRIORITY; + +#ifdef RF_PRESENT +#define freq RF12_433MHZ // Use the freq to match the module you have. + +const int nodeID = 10; +const int networkGroup = 210; +const int UNO = 1; +#endif + +typedef struct { + int power_L1; + int power_L2; + int power_L3; + int Vrms_L1; + int Vrms_L2; + int Vrms_L3;} Tx_struct; // revised data for RF comms +Tx_struct tx_data; + + +// ----------- Pinout assignments ----------- +// +// digital pins: +const byte loadPrioritySelectorPin = 3; // // for 3-phase PCB +// D4 is not in use +const byte physicalLoad_0_pin = 5; // for 3-phase PCB, Load #1 (Rev 2 PCB) +const byte physicalLoad_1_pin = 6; // for 3-phase PCB, Load #2 (Rev 2 PCB) +const byte physicalLoad_2_pin = 7; // for 3-phase PCB, Load #3 (Rev 2 PCB) +// D8 is not in use +// D9 is not in use + +// analogue input pins +const byte sensorV[NO_OF_PHASES] = {0,2,4}; // for 3-phase PCB +const byte sensorI[NO_OF_PHASES] = {1,3,5}; // for 3-phase PCB + + +// -------------- general global variables ----------------- +// +// Some of these variables are used in multiple blocks so cannot be static. +// For integer maths, some variables need to be 'long' +// +boolean beyondStartUpPeriod = false; // start-up delay, allows things to settle +byte initialDelay = 3; // in seconds, to allow time to open the Serial monitor +byte startUpPeriod = 3; // in seconds, to allow LP filter to settle + +long DCoffset_V_long[NO_OF_PHASES]; // <--- for LPF +long DCoffset_V_min; // <--- for LPF (min limit) +long DCoffset_V_max; // <--- for LPF (max limit) +int DCoffset_I_nom; // nominal mid-point value of ADC @ x1 scale + +// for 3-phase use, with units of Joules * CYCLES_PER_SECOND +float capacityOfEnergyBucket_main; +float energyInBucket_main; +float midPointOfEnergyBucket_main; +float lowerThreshold_default; +float lowerEnergyThreshold; +float upperThreshold_default; +float upperEnergyThreshold; +float offsetOfEnergyThresholdsInAFmode = 0.1; // <-- must not exceeed 0.4 + +// for improved control of multiple loads +boolean recentTransition = false; +byte postTransitionCount; +#define POST_TRANSITION_MAX_COUNT 3 // <-- allows each transition to take effect +//#define POST_TRANSITION_MAX_COUNT 50 // <-- for testing only +byte activeLoad = 0; + +// for datalogging +int datalogCountInMainsCycles; +const int maxDatalogCountInMainsCycles = DATALOG_PERIOD_IN_SECONDS * CYCLES_PER_SECOND; +float energyStateOfPhase[NO_OF_PHASES]; // only used for datalogging + +// for interaction between the main processor and the ISR +volatile boolean dataReady = false; +volatile int sampleV[NO_OF_PHASES]; +volatile int sampleI[NO_OF_PHASES]; + +// For a mechanism to check the continuity of the sampling sequence +#define CONTINUITY_CHECK_MAXCOUNT 250 // mains cycles +int mainsCycles_forContinuityChecker; +int lowestNoOfSampleSetsPerMainsCycle; + +// Calibration values +//------------------- +// Three calibration values are used in this sketch: powerCal, phaseCal and voltageCal. +// With most hardware, the default values are likely to work fine without +// need for change. A compact explanation of each of these values now follows: + +// When calculating real power, which is what this code does, the individual +// conversion rates for voltage and current are not of importance. It is +// only the conversion rate for POWER which is important. This is the +// product of the individual conversion rates for voltage and current. It +// therefore has the units of ADC-steps squared per Watt. Most systems will +// have a power conversion rate of around 20 (ADC-steps squared per Watt). +// +// powerCal is the RECIPR0CAL of the power conversion rate. A good value +// to start with is therefore 1/20 = 0.05 (Watts per ADC-step squared) +// +const float powerCal[NO_OF_PHASES] = {0.043, 0.043, 0.043}; + +// phaseCal is used to alter the phase of the voltage waveform relative to the +// current waveform. The algorithm interpolates between the most recent pair +// of voltage samples according to the value of phaseCal. +// +// With phaseCal = 1, the most recent sample is used. +// With phaseCal = 0, the previous sample is used +// With phaseCal = 0.5, the mid-point (average) value in used +// +// NB. Any tool which determines the optimal value of phaseCal must have a similar +// scheme for taking sample values as does this sketch. +// +const float phaseCal[NO_OF_PHASES] = {0.5, 0.5, 0.5}; // <- nominal values only +int phaseCal_int[NO_OF_PHASES]; // to avoid the need for floating-point maths + +// For datalogging purposes, voltageCal has been added too. Because the range of ADC values is +// similar to the actual range of volts, the optimal value for this cal factor is likely to be +// close to unity. +const float voltageCal[NO_OF_PHASES] = {1.03, 1.03, 1.03}; // compared with Fluke 77 meter + + + + +void setup() +{ + delay (initialDelay * 1000); // allows time to open the Serial Monitor + + Serial.begin(9600); // initialize Serial interface + Serial.println(); + Serial.println(); + Serial.println(); + Serial.println("----------------------------------"); + Serial.println("Sketch ID: Mk2_3phase_RFdatalog_4.ino"); + + pinMode(physicalLoad_0_pin, OUTPUT); // driver pin for Load #1 + pinMode(physicalLoad_1_pin, OUTPUT); // driver pin for Load #2 + pinMode(physicalLoad_2_pin, OUTPUT); // driver pin for Load #3 + + for(int i = 0; i< noOfDumploads; i++) + { + logicalLoadState[i] = LOAD_OFF; + } + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // update the local load's state. + digitalWrite(physicalLoad_1_pin, physicalLoadState[1]); // update the additional load state (inverse logic). + digitalWrite(physicalLoad_2_pin, physicalLoadState[2]); // update the additional load state (inverse logic). + + pinMode(loadPrioritySelectorPin, INPUT); + digitalWrite(loadPrioritySelectorPin, HIGH); // enable the internal pullup resistor + delay (100); // allow time to settle + int pinState = digitalRead(loadPrioritySelectorPin); // initial selection and + loadPriorityMode = (enum loadPriorityModes)pinState; // assignment of priority mode + + for (byte phase = 0; phase < NO_OF_PHASES; phase++) + { + // When using integer maths, calibration values that have been supplied in + // floating point form need to be rescaled. + phaseCal_int[phase] = phaseCal[phase] * 256; // for integer maths + DCoffset_V_long[phase] = 512L * 256; // nominal mid-point value of ADC @ x256 scale + } + + // Define operating limits for the LP filters which identify DC offset in the voltage + // sample streams. By limiting the output range, these filters always should start up + // correctly. + DCoffset_V_min = (long)(512L - 100) * 256; // mid-point of ADC minus a working margin + DCoffset_V_max = (long)(512L + 100) * 256; // mid-point of ADC plus a working margin + DCoffset_I_nom = 512; // nominal mid-point value of ADC @ x1 scale + + // for the main energy bucket + capacityOfEnergyBucket_main = (float)WORKING_ZONE_IN_JOULES * CYCLES_PER_SECOND; + midPointOfEnergyBucket_main = capacityOfEnergyBucket_main * 0.5; // for resetting flexible thresholds + energyInBucket_main = 0; + + Serial.println ("ADC mode: free-running"); + Serial.print ("requiredExport in Watts = "); + Serial.println (REQUIRED_EXPORT_IN_WATTS); + + // Set up the ADC to be free-running + ADCSRA = (1< 50) + { + count = 0; + del++; // increase delay by 1uS + } + } +#endif + + } // <-- this closing brace needs to be outside the WORKLOAD_CHECK blocks! + +#ifdef WORKLOAD_CHECK + switch (displayFlag) + { + case 0: // the result is available now, but don't display until the next loop + displayFlag++; + break; + case 1: // with minimum delay, it's OK to print now + Serial.print(res); + displayFlag++; + break; + case 2: // with minimum delay, it's OK to print now + Serial.println("uS"); + displayFlag++; + break; + default:; // for most of the time, displayFlag is 3 + } +#endif + +} // end of loop() + + +// This routine is called to process each set of V & I samples (3 pairs). The main processor and +// the ADC work autonomously, their operation being synchnonised only via the dataReady flag. +// +void processRawSamples() +{ + static long sumP[NO_OF_PHASES]; + static enum polarities polarityOfLastSampleV[NO_OF_PHASES]; // for zero-crossing detection + static long lastSampleV_minusDC_long[NO_OF_PHASES]; // for the phaseCal algorithm + static long cumVdeltasThisCycle_long[NO_OF_PHASES]; // for the LPF which determines DC offset (voltage) + static int samplesDuringThisMainsCycle[NO_OF_PHASES]; + static long sum_Vsquared[NO_OF_PHASES]; + static long samplesDuringThisDatalogPeriod; + enum polarities polarityNow; + + // The raw V and I samples are processed in "phase pairs" + for (byte phase = 0; phase < NO_OF_PHASES; phase++) + { + // remove DC offset from each raw voltage sample by subtracting the accurate value + // as determined by its associated LP filter. + long sampleV_minusDC_long = ((long)sampleV[phase]<<8) - DCoffset_V_long[phase]; + + // determine polarity, to aid the logical flow + if(sampleV_minusDC_long > 0) { + polarityNow = POSITIVE; } + else { + polarityNow = NEGATIVE; } + + if (polarityNow == POSITIVE) + { + if (polarityOfLastSampleV[phase] != POSITIVE) + { + if (beyondStartUpPeriod) + { + // This is the start of a new +ve half cycle, for this phase, just after the + // zero-crossing point. Before the contribution from this phase can be added + // to the running total, the cal factor for this phase must be applied. + // + float realPower = (sumP[phase] / samplesDuringThisMainsCycle[phase]) * powerCal[phase]; + + processLatestContribution(phase, realPower); // runs at 6.6 ms intervals + + // A performance check to monitor and display the minimum number of sets of + // ADC samples per mains cycle, the expected number being 20ms / (104us * 6) = 32.05 + // + if (phase == 0) + { + if (samplesDuringThisMainsCycle[phase] < lowestNoOfSampleSetsPerMainsCycle) + { + lowestNoOfSampleSetsPerMainsCycle = samplesDuringThisMainsCycle[phase]; + } + mainsCycles_forContinuityChecker++; + if (mainsCycles_forContinuityChecker >= CONTINUITY_CHECK_MAXCOUNT) + { + mainsCycles_forContinuityChecker = 0; + Serial.println(lowestNoOfSampleSetsPerMainsCycle); + lowestNoOfSampleSetsPerMainsCycle = 999; + } + } + + sumP[phase] = 0; + samplesDuringThisMainsCycle[phase] = 0; + + } // end of processing that is specific to the first Vsample in each +ve half cycle + else + { + // wait until the DC-blocking filters have had time to settle + if(millis() > (initialDelay + startUpPeriod) * 1000) + { + beyondStartUpPeriod = true; + mainsCycles_forContinuityChecker = 1; // opportunity has been missed for this cycle + lowestNoOfSampleSetsPerMainsCycle = 999; + Serial.println ("Go!"); + } + } + } // end of processing that is specific to the first Vsample in each +ve half cycle + + // still processing samples where the voltage is POSITIVE ... + // check to see whether the trigger device can now be reliably armed + if ((phase == 0) && samplesDuringThisMainsCycle[0] == 2) // lower value for larger sample set + { + if (beyondStartUpPeriod) + { + // This code is executed once per 20mS, shortly after the start of each new + // mains cycle on phase 0. + // + datalogCountInMainsCycles++; + + // Changling the state of the loads is is a 3-part process: + // - change the LOGICAL load states as necessary to maintain the energy level + // - update the PHYSICAL load states according to the logical -> physical mapping + // - update the driver lines for each of the loads. + // + // Restrictions apply for the period immediately after a load has been switched. + // Here the recentTransition flag is checked and updated as necessary. + if (recentTransition) + { + postTransitionCount++; + if (postTransitionCount >= POST_TRANSITION_MAX_COUNT) + { + recentTransition = false; + } + } + + if (energyInBucket_main > midPointOfEnergyBucket_main) + { + // the energy state is in the upper half of the working range + lowerEnergyThreshold = lowerThreshold_default; // reset the "opposite" threshold + if (energyInBucket_main > upperEnergyThreshold) + { + // Because the energy level is high, some action may be required + boolean OK_toAddLoad = true; + byte tempLoad = nextLogicalLoadToBeAdded(); + if (tempLoad < noOfDumploads) + { + // a load which is now OFF has been identified for potentially being switched ON + if (recentTransition) + { + // During the post-transition period, any increase in the energy level is noted. + upperEnergyThreshold = energyInBucket_main; + + // the energy thresholds must remain within range + if (upperEnergyThreshold > capacityOfEnergyBucket_main) + { + upperEnergyThreshold = capacityOfEnergyBucket_main; + } + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toAddLoad = false; + } + } + + if (OK_toAddLoad) + { + logicalLoadState[tempLoad] = LOAD_ON; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + Serial.print('+'); + Serial.println(activeLoad); + } + } + } + } + else + { // the energy state is in the lower half of the working range + upperEnergyThreshold = upperThreshold_default; // reset the "opposite" threshold + if (energyInBucket_main < lowerEnergyThreshold) + { + // Because the energy level is low, some action may be required + boolean OK_toRemoveLoad = true; + byte tempLoad = nextLogicalLoadToBeRemoved(); + if (tempLoad < noOfDumploads) + { + // a load which is now ON has been identified for potentially being switched OFF + if (recentTransition) + { + // During the post-transition period, any decrease in the energy level is noted. + lowerEnergyThreshold = energyInBucket_main; + + // the energy thresholds must remain within range + if (lowerEnergyThreshold < 0) + { + lowerEnergyThreshold = 0; + } + + // Only the active load may be switched during this period. All other loads must + // wait until the recent transition has had sufficient opportunity to take effect. + if (tempLoad != activeLoad) + { + OK_toRemoveLoad = false; + } + } + + if (OK_toRemoveLoad) + { + logicalLoadState[tempLoad] = LOAD_OFF; + activeLoad = tempLoad; + postTransitionCount = 0; + recentTransition = true; + Serial.print('-'); + Serial.println(activeLoad); + } + } + } + } + + updatePhysicalLoadStates(); // allows the logical-to-physical mapping to be changed + + // update the control ports for each of the physical loads + digitalWrite(physicalLoad_0_pin, physicalLoadState[0]); // active low for trigger + digitalWrite(physicalLoad_1_pin, physicalLoadState[1]); // active low for trigger + digitalWrite(physicalLoad_2_pin, physicalLoadState[2]); // active low for trigger + + // Now that the energy-related decisions have been taken, min and max limits can now + // be applied to the level of the energy bucket. This is to ensure correct operation + // when conditions change, i.e. when import changes to export, and vici versa. + // + if (energyInBucket_main > capacityOfEnergyBucket_main) { + energyInBucket_main = capacityOfEnergyBucket_main; } + else + if (energyInBucket_main < 0) { + energyInBucket_main = 0; } + + if (datalogCountInMainsCycles >= maxDatalogCountInMainsCycles) + { + datalogCountInMainsCycles = 0; + + // To provide sufficient range for a dataloging period of at least 20 seconds, the accumulator + // for Vsquared is now scaled at 1/16 of its previous V_ADC * V_ADC value. + // Hence the * 4 factor that appears below after the sqrt() operation below. + // + tx_data.power_L1 = energyStateOfPhase[0] / maxDatalogCountInMainsCycles; + tx_data.power_L1 *= -1; // to match the OEM convention (import is =ve; export is -ve) + tx_data.power_L2 = energyStateOfPhase[1] / maxDatalogCountInMainsCycles; + tx_data.power_L2 *= -1; // to match the OEM convention (import is =ve; export is -ve) + tx_data.power_L3 = energyStateOfPhase[2] / maxDatalogCountInMainsCycles; + tx_data.power_L3 *= -1; // to match the OEM convention (import is =ve; export is -ve) + tx_data.Vrms_L1 = (int)(voltageCal[0] * sqrt(sum_Vsquared[0] / samplesDuringThisDatalogPeriod) * 4); + tx_data.Vrms_L2 = (int)(voltageCal[1] * sqrt(sum_Vsquared[1] / samplesDuringThisDatalogPeriod) * 4); + tx_data.Vrms_L3 = (int)(voltageCal[2] * sqrt(sum_Vsquared[2] / samplesDuringThisDatalogPeriod) * 4); +#ifdef RF_PRESENT + send_rf_data(); +#endif +/* <-- Warning - Unlike its 1-phase equivalent, this 3-phase code can be affected by Serial statements! + Serial.print(energyInBucket_main / CYCLES_PER_SECOND); + Serial.print(", "); + Serial.println(tx_data.power_L1); + Serial.print(", "); + Serial.print(tx_data.power_L2); + Serial.print(", "); + Serial.print(tx_data.power_L3); + Serial.print(", "); + Serial.println(tx_data.Vrms_L1); + Serial.print(", "); + Serial.print(tx_data.Vrms_L2); + Serial.print(", "); + Serial.println(tx_data.Vrms_L3); +*/ + energyStateOfPhase[0] = 0; + energyStateOfPhase[1] = 0; + energyStateOfPhase[2] = 0; + sum_Vsquared[0] = 0; + sum_Vsquared[1] = 0; + sum_Vsquared[2] = 0; + samplesDuringThisDatalogPeriod = 0; + + +/* <-- Warning - Unlike its 1-phase equivalent, this 3-phase code can be affected by Serial statements! + for (int i = 0; i < noOfDumploads; i++) + { + Serial.print(logicalLoadState[i]); + } + Serial.println(); +*/ + } + } + } + } // end of processing that is specific to samples where the voltage is positive + + else // the polarity of this sample is negative + { + if (polarityOfLastSampleV[phase] != NEGATIVE) + { + // This is the start of a new -ve half cycle (just after the zero-crossing point) + // This is a convenient point to update the Low Pass Filter for removing the DC + // component from the phase that is being processed. + // The portion which is fed back into the integrator is approximately one percent + // of the average offset of all the Vsamples in the previous mains cycle. + // + DCoffset_V_long[phase] += (cumVdeltasThisCycle_long[phase]>>12); + cumVdeltasThisCycle_long[phase] = 0; + + // To ensure that this LP filter will always start up correctly when 240V AC is + // available, its output value needs to be prevented from drifting beyond the likely range + // of the voltage signal. + // + if (DCoffset_V_long[phase] < DCoffset_V_min) { + DCoffset_V_long[phase] = DCoffset_V_min; } + else + if (DCoffset_V_long[phase] > DCoffset_V_max) { + DCoffset_V_long[phase] = DCoffset_V_max; } + + if (phase == 0) + { + checkLoadPrioritySelection(); // updates load priorities if the switch is changed + } + + } // end of processing that is specific to the first Vsample in each -ve half cycle + } // end of processing that is specific to samples where the voltage is negative + + // Processing for EVERY pair of samples. Most of this code is not used during the + // start-up period, but it does no harm to leave it in place. Accumulated values + // are cleared when the beyondStartUpPhase flag is set to true. + // + // remove most of the DC offset from the current sample (the precise value does not matter) + long sampleIminusDC_long = ((long)(sampleI[phase] - DCoffset_I_nom))<<8; + + // phase-shift the voltage waveform so that it aligns with the current when a + // resistive load is used + long phaseShiftedSampleV_minusDC_long = lastSampleV_minusDC_long[phase] + + (((sampleV_minusDC_long - lastSampleV_minusDC_long[phase])*phaseCal_int[phase])>>8); + + // calculate the "real power" in this sample pair and add to the accumulated sum + long filtV_div4 = phaseShiftedSampleV_minusDC_long>>2; // reduce to 16-bits (x64, or 2^6) + long filtI_div4 = sampleIminusDC_long>>2; // reduce to 16-bits (x64, or 2^6) + long instP = filtV_div4 * filtI_div4; // 32-bits (x4096, or 2^12) + instP = instP>>12; // reduce to 20-bits (x1) + sumP[phase] +=instP; // scaling is x1 + + // for the Vrms calculation (for datalogging only) + long inst_Vsquared = filtV_div4 * filtV_div4; // 32-bits (x4096, or 2^12) + // inst_Vsquared = inst_Vsquared>>12; // 20-bits (x1), not enough range :-( + inst_Vsquared = inst_Vsquared>>16; // 16-bits (x1/16, or 2^-4), for more datalog range :-) + sum_Vsquared[phase] += inst_Vsquared; // scaling is x1/16 + if (phase == 0) { + samplesDuringThisDatalogPeriod ++; } // no need to keep separate counts for each phase + + // general housekeeping + cumVdeltasThisCycle_long[phase] += sampleV_minusDC_long; // for use with LP filter + samplesDuringThisMainsCycle[phase] ++; + + // store items for use during next loop + lastSampleV_minusDC_long[phase] = sampleV_minusDC_long; // required for phaseCal algorithm + polarityOfLastSampleV[phase] = polarityNow; // for identification of half cycle boundaries + } +} +// end of processRawSamples() + + +void processLatestContribution(byte phase, float power) +{ + float latestEnergyContribution = power; // for efficiency, the energy scale is Joules * CYCLES_PER_SECOND + + // add the latest energy contribution to the relevant per-phase accumulator + // (only used for datalogging of power) + energyStateOfPhase[phase] += latestEnergyContribution; + + // add the latest energy contribution to the main energy accumulator + energyInBucket_main += latestEnergyContribution; + + // apply any adjustment that is required. + if (phase == 0) + { + energyInBucket_main -= REQUIRED_EXPORT_IN_WATTS; // energy scale is Joules x 50 + } + + // Applying max and min limits to the main accumulator's level + // is deferred until after the energy related decisions have been taken + // +} + +byte nextLogicalLoadToBeAdded() +{ + byte retVal = noOfDumploads; + boolean success = false; + + for (byte index = 0; index < noOfDumploads && !success; index++) + { + if (logicalLoadState[index] == LOAD_OFF) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +byte nextLogicalLoadToBeRemoved() +{ + byte retVal = noOfDumploads; + boolean success = false; + + // NB. the index cannot be a 'byte' because the loop would not terminate correctly! + for (char index = (noOfDumploads -1); index >= 0 && !success; index--) + { + if (logicalLoadState[index] == LOAD_ON) + { + success = true; + retVal = index; + } + } + return(retVal); +} + + +void updatePhysicalLoadStates() +/* + * This function provides the link between the logical and physical loads. The + * array, logicalLoadState[], contains the on/off state of all logical loads, with + * element 0 being for the one with the highest priority. The array, + * physicalLoadState[], contains the on/off state of all physical loads. + * + * The association between the physical and logical loads is 1:1. By default, numerical + * equivalence is maintained, so logical(N) maps to physical(N). If physical load 1 is set + * to have priority, rather than physical load 0, the logical-to-physical association for + * loads 0 and 1 are swapped. + * + * Any other mapping relaionships could be configured here. + */ +{ + for (int i = 0; i < noOfDumploads; i++) + { + physicalLoadState[i] = logicalLoadState[i]; + } + + if (loadPriorityMode == LOAD_1_HAS_PRIORITY) + { + // swap physical loads 0 & 1 if remote load has priority + physicalLoadState[0] = logicalLoadState[1]; + physicalLoadState[1] = logicalLoadState[0]; + } +} + + +// this function changes the value of the load priorities if the state of the external switch is altered +void checkLoadPrioritySelection() +{ + static byte loadPrioritySwitchCcount = 0; + int pinState = digitalRead(loadPrioritySelectorPin); + if (pinState != loadPriorityMode) + { + loadPrioritySwitchCcount++; + } + if (loadPrioritySwitchCcount >= 20) + { + loadPrioritySwitchCcount = 0; + loadPriorityMode = (enum loadPriorityModes)pinState; // change the global variable + Serial.print ("loadPriority selection changed to "); + if (loadPriorityMode == LOAD_0_HAS_PRIORITY) { + Serial.println ( "load 0"); } + else { + Serial.println ( "load 1"); } + } +} + + +// Although this sketch always operates in ANTI_FLICKER mode, it was convenient +// to leave this mechanism in place. +// +void configureParamsForSelectedOutputMode() +{ + if (outputMode == ANTI_FLICKER) + { + // settings for anti-flicker mode + lowerThreshold_default = + capacityOfEnergyBucket_main * (0.5 - offsetOfEnergyThresholdsInAFmode); + upperThreshold_default = + capacityOfEnergyBucket_main * (0.5 + offsetOfEnergyThresholdsInAFmode); + } + else + { + // settings for normal mode + lowerThreshold_default = capacityOfEnergyBucket_main * 0.5; + upperThreshold_default = capacityOfEnergyBucket_main * 0.5; + } + + // display relevant settings for selected output mode + Serial.print(" capacityOfEnergyBucket_main = "); + Serial.println(capacityOfEnergyBucket_main); + Serial.print(" lowerEnergyThreshold = "); + Serial.println(lowerThreshold_default); + Serial.print(" upperEnergyThreshold = "); + Serial.println(upperThreshold_default); + + Serial.print(">>free RAM = "); + Serial.println(freeRam()); // a useful value to keep an eye on +} + + +#ifdef RF_PRESENT +void send_rf_data() +{ + int i = 0; + while (!rf12_canSend() && i<10) + { + rf12_recvDone(); + i++; + } + rf12_sendStart(0, &tx_data, sizeof tx_data); +} +#endif + + +int freeRam () { + extern int __heap_start, *__brkval; + int v; + return (int) &v - (__brkval == 0 ? (int) &__heap_start : (int) __brkval); +} + diff --git a/docs/routers/rem81/router-esphome.yaml b/docs/routers/rem81/router-esphome.yaml new file mode 100644 index 0000000..50955ec --- /dev/null +++ b/docs/routers/rem81/router-esphome.yaml @@ -0,0 +1,673 @@ +substitutions: + device_name: "esp176-esp32-routeur-1r" + friendly_name: esp176 + adress_ip: "192.168.0.176" + time_timezone: "Europe/Paris" + +esphome: + name: ${device_name} + platform: ESP32 + board: esp32dev + project: + name: "rem81.esp176-esp32-routeur" + version: "1.2.1" + on_boot: + priority: 800 + # Force mode auto et temperature ok au demarrage + then: + - switch.turn_on: modeauto + - binary_sensor.template.publish: + id: temperatureok + state: ON + +wifi: + networks: + - ssid: !secret wifi + password: !secret mdpwifi + reboot_timeout: 5min + + manual_ip: + static_ip: ${adress_ip} + gateway: 192.168.0.254 + subnet: 255.255.255.0 + +# Enable logging +logger: + baud_rate: 0 + level: DEBUG + # level: INFO + +api: + +ota: + +web_server: + port: 80 + +# Protocole afficheur +i2c: + sda: GPIO21 + scl: GPIO22 + scan: True + id: bus_a + +# Protocole du JSK +uart: + id: mod_bus + tx_pin: 17 + rx_pin: 16 + baud_rate: 38400 + stop_bits: 1 +# debug: +# direction: BOTH +# dummy_receiver: false +# after: +# timeout: 150ms +# sequence: +# - lambda: |- +# UARTDebug::log_string(direction, bytes); + +modbus: + #flow_control_pin: 5 + #send_wait_time: 200ms + id: modbus1 + +modbus_controller: + - id: jsymk + ## the Modbus device addr + address: 0x1 + modbus_id: modbus1 + update_interval: 0.75s + command_throttle: 50ms + + # setup_priority: -10 + +globals: + - id: increment + type: float + restore_value: no + initial_value: "0" + - id: striac + type: float + restore_value: yes + +# Sonde Temperature Dallas +dallas: + - pin: GPIO27 # + update_interval: 60s + +# Informations supplementaires sur le WIFI +#text_sensor: +# - platform: wifi_info +# ip_address: +# name: ${friendly_name}_ESP IP Address +# ssid: +# name: ${friendly_name}_ESP Connected SSID +# bssid: +# name: ${friendly_name}_ESP Connected BSSID +# mac_address: +# name: ${friendly_name}_ESP Mac Wifi Address +# scan_results: +# name: ${friendly_name}_ESP Latest Scan Results + +binary_sensor: + #Etat de la connection + - platform: status + name: "${friendly_name}_Status" + + # Temperature triac OK + - platform: template + name: "${friendly_name} Temp Ok" + id: temperatureok + + # Validation du routeur à déclarer dans HA + - platform: homeassistant + name: "Validation Routeur" + entity_id: "input_boolean.inter_validation_routeur" + publish_initial_state: true + id: val_routeur + +# Input Number +number: + # Seuil Max sortie triac + - platform: template + name: "${friendly_name} P Max" + id: pmax + optimistic: true + restore_value: true + mode: box + min_value: 50 + max_value: 100 + unit_of_measurement: "%" + step: 1 + + # Seuil MAX temperature + - platform: template + name: "${friendly_name} T Max" + id: tmax + optimistic: true + restore_value: true + mode: box + min_value: 0 + max_value: 75 + unit_of_measurement: "C°" + step: 0.1 + + # Coeff Réactivité + - platform: template + name: "${friendly_name} Coeff R" + id: coeff_r + optimistic: true + restore_value: true + mode: box + min_value: 0 + max_value: 10 + unit_of_measurement: "" + step: 0.1 + +sensor: + # tension de l'alimentation + - platform: modbus_controller + modbus_controller_id: jsymk + id: Tension + #name: "${friendly_name} Tension JSYMK" + address: 0x0048 + unit_of_measurement: "V" + register_type: holding + value_type: U_DWORD + accuracy_decimals: 1 + filters: + - multiply: 0.0001 + register_count: 1 + response_size: 4 + + # Intensité traversant le tore + - platform: modbus_controller + modbus_controller_id: jsymk + id: Itore + name: "${friendly_name} I_ECS JSYMK" + address: 0x0049 + unit_of_measurement: "A" + register_type: holding + value_type: U_DWORD + accuracy_decimals: 1 + filters: + - multiply: 0.0001 + register_count: 1 + response_size: 4 + + # Puissance traversant le tore + - platform: modbus_controller + modbus_controller_id: jsymk + id: puecs + name: "${friendly_name} P_ECS JSYMK" + address: 0x004A + unit_of_measurement: "W" + register_type: holding + value_type: U_DWORD + accuracy_decimals: 1 + filters: + - multiply: 0.0001 + register_count: 1 + response_size: 4 + + # Energie lue dans le tore + - platform: modbus_controller + modbus_controller_id: jsymk + id: energietore + name: "${friendly_name} Energie ECS JSYMK" + address: 0x004B + unit_of_measurement: "kWh" + register_type: holding + value_type: U_DWORD + accuracy_decimals: 1 + filters: + - multiply: 0.0001 + register_count: 1 + response_size: 4 + + # FP lue dans le tore + - platform: modbus_controller + modbus_controller_id: jsymk + id: fptore + #name: "${friendly_name} FP Tore JSYMK" + address: 0x004C + register_type: holding + value_type: U_DWORD + accuracy_decimals: 1 + filters: + - multiply: 0.0001 + register_count: 1 + response_size: 4 + + # Energie NEG lue dans le tore + - platform: modbus_controller + modbus_controller_id: jsymk + id: energietoren + name: "${friendly_name} Energie ECS Neg JSYMK" + address: 0x004D + unit_of_measurement: "kWh" + register_type: holding + value_type: U_DWORD + accuracy_decimals: 1 + filters: + - multiply: 0.0001 + register_count: 1 + response_size: 4 + # Sens du courant dans la pince + - platform: modbus_controller + modbus_controller_id: jsymk + id: senspince + #name: "${friendly_name} Sens_Pince JSYMK" + address: 0x004E + register_type: holding + value_type: U_DWORD + bitmask: 0X00010000 + filters: + - multiply: 1 + register_count: 1 + response_size: 4 + # Sens du courant dans le tore + - platform: modbus_controller + modbus_controller_id: jsymk + id: senstor + #name: "${friendly_name} Sens_Tore JSYMK" + address: 0x004E + register_type: holding + value_type: U_DWORD + accuracy_decimals: 0 + bitmask: 0X01000000 + filters: + - multiply: 1 + register_count: 1 + response_size: 4 + + # Fréquence de l'alimentation + - platform: modbus_controller + modbus_controller_id: jsymk + id: frequence + #name: "${friendly_name} Frequence JSYMK" + address: 0x004F + unit_of_measurement: "hz" + register_type: holding + value_type: U_DWORD + accuracy_decimals: 1 + filters: + - multiply: 0.01 + register_count: 1 + response_size: 4 + + # tension de l'alimentation + - platform: modbus_controller + modbus_controller_id: jsymk + id: Tension2 + #name: "${friendly_name} U_Reseau JSYMK" + address: 0x0050 + unit_of_measurement: "V" + register_type: holding + value_type: U_DWORD + accuracy_decimals: 1 + filters: + - multiply: 0.0001 + register_count: 1 + response_size: 4 + + # Intensité lue dans la pince + - platform: modbus_controller + modbus_controller_id: jsymk + id: Ireseau + #name: "${friendly_name} I_Reseau JSYMK" + address: 0x0051 + unit_of_measurement: "A" + register_type: holding + value_type: U_DWORD + accuracy_decimals: 1 + filters: + - multiply: 0.0001 + register_count: 1 + response_size: 4 + + # puissance lue dans la pince + - platform: modbus_controller + modbus_controller_id: jsymk + id: pureseau + #name: "${friendly_name} P_Reseau JSYMK" + address: 0x0052 + unit_of_measurement: "W" + register_type: holding + value_type: U_DWORD + accuracy_decimals: 1 + filters: + - multiply: 0.0001 + register_count: 1 + response_size: 4 + on_value: + then: + - lambda: |- + if ( id(senspince).state == 1 ) { + id(pureseau1).publish_state( id(pureseau).state *-1); + } else { + id(pureseau1).publish_state( id(pureseau).state ); + } + + # Energie lue dans la pince + - platform: modbus_controller + modbus_controller_id: jsymk + id: energiepince + #name: "${friendly_name} Energie Reseau JSYMK" + address: 0x0053 + unit_of_measurement: "kWh" + register_type: holding + value_type: U_DWORD + accuracy_decimals: 1 + filters: + - multiply: 0.0001 + register_count: 1 + response_size: 4 + + # Energie lue dans le tore + - platform: modbus_controller + modbus_controller_id: jsymk + id: fppince + #name: "${friendly_name} FP Pince JSYMK" + address: 0x0054 + register_type: holding + value_type: U_DWORD + accuracy_decimals: 1 + filters: + - multiply: 0.0001 + register_count: 1 + response_size: 4 + + # Energie NEG lue dans le tore + - platform: modbus_controller + modbus_controller_id: jsymk + id: energienegpince + #name: "${friendly_name} Energie ECS Neg JSYMK" + address: 0x0055 + unit_of_measurement: "kWh" + register_type: holding + value_type: U_DWORD + accuracy_decimals: 1 + filters: + - multiply: 0.0001 + register_count: 1 + response_size: 4 + + # Informations WI_FI + - platform: wifi_signal # Affiche le signal WiFi strength/RSSI en dB + name: "${friendly_name} WiFi Signal dB" + id: wifi_signal_db + update_interval: 60s + entity_category: "diagnostic" + + - platform: copy # Affiche le signal WiFi strength en % + source_id: wifi_signal_db + name: "${friendly_name} WiFi Signal Percent" + filters: + - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0); + unit_of_measurement: "Signal %" + entity_category: "diagnostic" + + ############### TEMPLATE ######################" + # Affichage dans HA et sur l'afficheur + - platform: template + name: "${friendly_name} Pu Reseau" + id: pureseau1 + unit_of_measurement: "W" + state_class: "measurement" + + - platform: template + name: "${friendly_name} Increment" + id: afincrement + unit_of_measurement: "" + accuracy_decimals: 2 + state_class: "measurement" + + - platform: template + name: "${friendly_name} Sortie Triac" + id: afstriac + unit_of_measurement: "%" + state_class: "measurement" + accuracy_decimals: 2 + + # Sonde Temperature radiateur + - platform: dallas + address: 0xeb012112e461b128 + name: "${friendly_name} Temp triac" + id: temp_triac + filters: + - filter_out: NAN + +switch: + - platform: gpio + name: "${friendly_name} Relais" + pin: GPIO5 + id: relais + + - platform: template + name: "${friendly_name} Mode Auto" + id: modeauto + optimistic: true + restore_mode: always_on + + - platform: restart + name: "${friendly_name} Restart" + +output: + #LEDS -------------------------------------- + - id: led_conso + platform: gpio + pin: GPIO32 + + - id: led_injec + platform: gpio + pin: GPIO25 + # Pilotage du Dimmer ------------------------ + - platform: ac_dimmer + id: ecs + gate_pin: GPIO33 + method: leading + zero_cross_pin: + number: GPIO34 + mode: + input: true + inverted: yes + min_power: 5% + +light: + - platform: monochromatic + name: "${friendly_name}+STriac" + output: ecs + id: gradateur + default_transition_length: 50ms + +# Affichage +display: + - platform: lcd_pcf8574 + dimensions: 20x4 + address: 0x27 + update_interval: 2s + lambda: |- + it.printf(0,0,"Pr=%0.0fW",id(pureseau1).state); + it.printf(10,0,"Pe=%0.0fW ",id(puecs).state); + it.printf(0,1,"Tr=%0.1f%%",id(striac)); + it.printf(10,1,"Val:%s", id(val_routeur).state ? "OK" : "NOK"); + it.printf(0,2,"Tp=%0.1fc", id(temp_triac).state); + it.printf(10,2,"Etat=%s", id(temperatureok).state ? "OK" : "NOK"); + it.printf(0,3,"Mode=%s", id(modeauto).state ? "Auto" : "Manu"); + it.printf(10,3,"Inc=%0.1f ",id(increment)); + +interval: + - interval: 1s + then: + - script.execute: calcul_injection + + - interval: 2s + then: + - script.execute: etat_production + + # Script activation relais si le triac est au max pendant x sec + - interval: 60s + then: + - script.execute: calcul_relais_surprod + +# ------------------------ Scripts +script: + # + # ------------------------ Calcul puissance injection + - id: calcul_injection + mode: single + then: + - if: + condition: + lambda: "return id(temp_triac).state < (id(tmax).state-2);" + # Si Temp Triac inferieur au seuil-2° alors OK + then: + - binary_sensor.template.publish: + id: temperatureok + state: ON + # Dévalidation du seuil de température + - if: + condition: + lambda: "return id(temp_triac).state >= id(tmax).state;" + # Si Temp Triac supérieur ou égale au seuil alors NOK + then: + - binary_sensor.template.publish: + id: temperatureok + state: OFF + # Validation du seuil de temperature triac + - lambda: |- + id(increment) = id(pureseau1).state*id(coeff_r).state/1000*-1; + + - lambda: |- + id(striac) = id(striac)+id(increment); + + if (!isnan(id(striac))) { + id(striac) = id(striac)+id(increment); + }else{ + id(striac)=0; + } + + if (id(striac) <= 0){ + id(striac) = 0; + } else if(id(striac)>=id(pmax).state){ + id(striac) = id(pmax).state; + } + - logger.log: + format: "Log S Triac %f - Increment %f" + args: ["id(striac)", "id(increment)"] + level: "info" + + # Si Routeur Validé et mode auto et temperature ok alors on active le triac + - if: + condition: + and: + - binary_sensor.is_on: val_routeur + - switch.is_on: modeauto + - binary_sensor.is_on: temperatureok + + then: + - light.turn_on: + id: gradateur + brightness: !lambda |- + return id(striac)/100 ; + - logger.log: + format: "Log Auto OK STriac %f - Increment %f" + args: ["id(striac)", "id(increment)"] + level: "info" + + # Si mode routeur devalidé ou temp NOK alors on désactive le triac + - if: + condition: + and: + - switch.is_on: modeauto + - or: + - binary_sensor.is_off: val_routeur + - binary_sensor.is_off: temperatureok + + then: + - lambda: |- + id(striac) = 0; + id(increment) = 0; + - light.turn_off: gradateur + - logger.log: + format: "Log Auto NOk STriac %f - Increment %f" + args: ["id(striac)", "id(increment)"] + level: "info" + + # Si mode routeur manu + - if: + condition: + and: + - switch.is_off: modeauto + then: + - lambda: |- + id(striac) = 0; + id(increment) = 0; + - logger.log: + format: "Log Manu STriac %f - Increment %f" + args: ["id(striac)", "id(increment)"] + level: "info" + # Affichage STriac et Increment + - lambda: |- + id(afstriac).publish_state( id(striac) ); + id(afincrement).publish_state( id(increment) ); + + - logger.log: + format: "Log Fin STriac %f - Increment %f" + args: ["id(striac)", "id(increment)"] + level: "info" + + # ------------------------------------------- Pilotage led + - id: etat_production + mode: single + then: + - if: + condition: + sensor.in_range: + id: pureseau1 + below: 50 + above: -50 + then: + - output.turn_on: led_conso + - output.turn_on: led_injec + + - if: + condition: + sensor.in_range: + id: pureseau1 + above: 50 + then: + - output.turn_off: led_injec + - output.turn_on: led_conso + + - if: + condition: + sensor.in_range: + id: pureseau1 + below: -50 + then: + - output.turn_off: led_conso + - output.turn_on: led_injec + + # Calcul du relais de surproduction + - id: calcul_relais_surprod + mode: single + then: + # Si sortie triac > pmax-5%, ce qui signifie que le triac est au max sans effet, pendant plus de 30s + # alors on active le relais + # si triac <= 0 alors on desactive le relais + - if: + condition: + - lambda: "return id(striac)>=id(pmax).state-5;" + then: + - delay: 30s + - switch.turn_on: relais + - logger.log: "Relais Activé" + - if: + condition: + - lambda: "return id(striac)<=0;" + then: + - switch.turn_off: relais + - logger.log: "Relais Désactivé" diff --git a/docs/routers/routeur_le_professolaire b/docs/routers/routeur_le_professolaire new file mode 160000 index 0000000..91bc10c --- /dev/null +++ b/docs/routers/routeur_le_professolaire @@ -0,0 +1 @@ +Subproject commit 91bc10c8333ea0f9b680f33696b75dc61014f5e4 diff --git a/docs/routers/routeur_solaire b/docs/routers/routeur_solaire new file mode 160000 index 0000000..7e531f9 --- /dev/null +++ b/docs/routers/routeur_solaire @@ -0,0 +1 @@ +Subproject commit 7e531f90ea0d7681141c67925bb0c7a6a8fb6ca4 diff --git a/docs/routers/xlyric-PV-discharge-Dimmer-AC-Dimmer-KIT-Robotdyn b/docs/routers/xlyric-PV-discharge-Dimmer-AC-Dimmer-KIT-Robotdyn new file mode 160000 index 0000000..afcbcc2 --- /dev/null +++ b/docs/routers/xlyric-PV-discharge-Dimmer-AC-Dimmer-KIT-Robotdyn @@ -0,0 +1 @@ +Subproject commit afcbcc2de982c0116ae664884f36c1321435c410 diff --git a/docs/routers/xlyric-pv-router-esp32 b/docs/routers/xlyric-pv-router-esp32 new file mode 160000 index 0000000..555c754 --- /dev/null +++ b/docs/routers/xlyric-pv-router-esp32 @@ -0,0 +1 @@ +Subproject commit 555c7546d9bb8f865ad2796b1b08b584b88ef8dd diff --git a/docs/spec/BTA24-600B.pdf b/docs/spec/BTA24-600B.pdf new file mode 100644 index 0000000..c1db1a7 Binary files /dev/null and b/docs/spec/BTA24-600B.pdf differ diff --git a/docs/spec/EN_61000-3-2.pdf b/docs/spec/EN_61000-3-2.pdf new file mode 100644 index 0000000..b3f135b Binary files /dev/null and b/docs/spec/EN_61000-3-2.pdf differ diff --git a/docs/spec/Enedis-NOI-CPT_01E.pdf b/docs/spec/Enedis-NOI-CPT_01E.pdf new file mode 100644 index 0000000..bf07392 Binary files /dev/null and b/docs/spec/Enedis-NOI-CPT_01E.pdf differ diff --git a/docs/spec/Enedis-NOI-CPT_02E.pdf b/docs/spec/Enedis-NOI-CPT_02E.pdf new file mode 100644 index 0000000..93f2373 Binary files /dev/null and b/docs/spec/Enedis-NOI-CPT_02E.pdf differ diff --git a/docs/spec/Enedis-NOI-CPT_54E.pdf b/docs/spec/Enedis-NOI-CPT_54E.pdf new file mode 100644 index 0000000..47d3668 Binary files /dev/null and b/docs/spec/Enedis-NOI-CPT_54E.pdf differ diff --git a/docs/spec/JSY-Reset.txt b/docs/spec/JSY-Reset.txt new file mode 100644 index 0000000..fa3108e --- /dev/null +++ b/docs/spec/JSY-Reset.txt @@ -0,0 +1,28 @@ +https://forum-photovoltaique.fr/viewtopic.php?p=717586&sid=3db9427c877401e7d57c0ffbc6f50096#p717586 + +Departement/Region : 38 +Professionnel PV : NON +Contact : +Re: jsy-mk-194T module qui compte dans les 2 sens +par Thierry3838 » 21 sept. 2023 11:32 + +Bonjour, + +Voici les commandes a envoyé au jsy pour faire un reset des compteurs d'énergie (sauf erreur de ma part): +on a 2 bobines qui comptent en + et - donc : +- Positive energy 1 : +commande : 0x01, 0x10, 0x00, 0x4B, 0x00, 0x02, 0x04, 0x00, 0x00, 0x00, 0x00, 0xB6, 0x2C +réponse : 0110004B000231DE +-Negative energy 1 : +commande : 0x01, 0x10, 0x00, 0x4D, 0x00, 0x02, 0x04, 0x00, 0x00, 0x00, 0x00, 0x36, 0x06 +réponse : 0110004D0002D1DF +- Positive energy 2 : +commande : 0x01, 0x10, 0x00, 0x53, 0x00, 0x02, 0x04, 0x00, 0x00, 0x00, 0x00, 0xB6, 0x86 +réponse : 011000530002B1D9 +-Negative energy 2 : +commande : 0x01, 0x10, 0x00, 0x55, 0x00, 0x02, 0x04, 0x00, 0x00, 0x00, 0x00, 0x36, 0xAC +réponse : 01100055000251D8 + +Pour le calcul du CRC vous pouvez allez sur https://crccalc.com/ et choisir CRC-16/modbus + +Thierry \ No newline at end of file diff --git a/docs/spec/LSA-H3P SSVR-single-phase_en.pdf b/docs/spec/LSA-H3P SSVR-single-phase_en.pdf new file mode 100644 index 0000000..3645373 Binary files /dev/null and b/docs/spec/LSA-H3P SSVR-single-phase_en.pdf differ diff --git a/docs/spec/Sequelec_GP15_Linky_2017_03_01.pdf b/docs/spec/Sequelec_GP15_Linky_2017_03_01.pdf new file mode 100644 index 0000000..ccb9025 Binary files /dev/null and b/docs/spec/Sequelec_GP15_Linky_2017_03_01.pdf differ diff --git a/docs/update-trials.sh b/docs/update-trials.sh new file mode 100755 index 0000000..15f1633 --- /dev/null +++ b/docs/update-trials.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +set -e + +to_dir="docs/downloads/trials" +link="/downloads/trials" + +rm -f -r $to_dir/* +gh release download latest -D $to_dir -R mathieucarbou/YaSolR -p "YaSolR-*-trial-*.bin" --clobber +rm $to_dir/*-debug.bin | true +rm $to_dir/*-debug.FACTORY.bin | true + +echo "Markdown:" +echo "" + +for f in $(ls $to_dir); do + echo "- [$f]($link/$f)" +done