Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ESP32: TRIAC phase-dimming module #3023

Closed
wants to merge 15 commits into from

Conversation

jpeletier
Copy link
Contributor

@jpeletier jpeletier commented Jan 29, 2020

  • This PR is for the dev-esp32 branch rather than for master.
  • This PR is compliant with the other contributing guidelines as well (if not, please describe why).
  • I have thoroughly tested my contribution.
  • The code changes are reflected in the documentation at docs/*.

This module implements phase-dimming for TRIAC-based mains dimmers.

How phase-dimming works

Find here a brief description on how TRIAC-based dimming works.

In essence, phase dimming implies generating a square-wave PWM signal, with the special characteristic that it must be perfectly synchronized with mains, modulating it, and thus delivering more or less effective power to the load depending on the PWM signal duty cycle.

In order to do this, a dimmer module or circuit must be used to a) detect when the mains signal crosses 0V, to use it as a synchronization point and b) modulate mains. The module must isolate mains from the microcontroller.

Some reference hardware:

These modules come with a TRIAC whose gate is intended to be driven by a GPIO pin, isolated from mains by an optocoupler. These modules also come with a zero-crossing detector, also isolated, that raises a pin voltage when the AC mains sine wave signal crosses 0V.

Architecture of this nodemcu module

The above scheme is implemented in this module by dedicating ESP32's CPU1 entirely to this purpose... Phase dimming requires very accurate timing. Configuring timer interrupts in the "busy" CPU0 does not work properly, with FreeRTOS scheduler, WiFi and so on demanding their share of the CPU at random intervals, which would make dimmed lamps flicker.

Once the dimmer module is started, by means of dimmer.setup(), a busy loop is launched on CPU1 that monitors zero-crossing signals from the dimmer and turns on/off the TRIAC at the appropriate time, with nanosecond precision.

Required SDK menuconfig changes

To use this module, change the following in menuconfig (make menuconfig):

  • Enable FreeRTOS in both cores by unselecting "Component Config/FreeRTOS/Run FreeRTOS only on first core". This will allow the module to use CPU1.
  • Unselect "Component Config/ESP32-specific/Also watch CPU1 tick interrupt"
  • Unselect "Component Config/ESP32-specific/Watch CPU1 idle task"

The last two settings disable the reset watchdog for CPU1. This is necessary since CPU1 is purposefully going to be running an infinite loop.

Example

dimmer.setup(14)     -- configure pin 14 as zero-crossing detector
dimmer.add(22)       -- TRIAC gate controlling lightbulb is connected to GPIO pin 22
dimmer.setLevel(300) -- set brightness to 300‰ (30%)

dimmer.setup()

Initializes the dimmer module. This will start a task that continuously monitors for signal zero crossing and turns on/off the different dimmed pins at the right time.

Syntax

dimmer.setup(zc_pin, [transition_speed])

Parameters

  • zc_pin: GPIO pin number where the zero crossing detector is connected. dimmer.setup() will configure this pin as input.
  • transition_speed: integer. Specifies a transition time or fade to be applied every time a brightness level is changed. It is defined as a per mille (‰) brightness delta per half mains cycle (10ms if 50Hz). For example, if set to 20, a light would go from zero to full brightness in 1000 / 20 * 10ms = 500ms
    • Value must be between 1 and 1000 (instant change).
    • Defaults to a quick ~100ms fade.

Returns

Throws errors if CPU1 task cannot be started or zc_pin cannot be configured as input.

It will reset if core 1 CPU is not active or their watchdogs are still enabled. See "Required SDK menuconfig changes" above.

dimmer.add()

Adds the provided pin to the dimmer module. The pin will be configured as output.

Syntax

dimmer.add(pin, [dimming_mode])

Parameters

  • pin: the GPIO pin to control.
  • dimming_mode: Dimming mode, either dimmer.LEADING_EDGE (default) or dimmer.TRAILING_EDGE, depending on the type of load or lightbulb.

Returns

dimmer.add() will throw an error if the pin was already added or if an incorrect GPIO pin is provided.

dimmer.remove()

Removes the given pin from the dimmer module

Syntax

dimmer.remove(pin)

Parameters

  • pin: the GPIO pin to stop controlling.

Returns

Will throw an error if the pin is not currently controlled by the module.

dimmer.setLevel()

Changes the brightness level for a dimmed pin

Syntax

dimmer.setLevel(pin, brightness)

Parameters

  • pin: Pin to configure
  • brightness: Integer. Per-mille (‰) brightness level or duty cycle, 0: off, 1000: fully on, or anything in between.

Returns

Will throw an error if attempting to configure a pin that was not added previously.

dimmer.mainsFrequency()

Returns the last measured mains frequency, in cHz (100ths of Hz). You must call dimmer.setup() first. Note that at least half a mains cycle must have passed (10ms at 50Hz) after dimmer.setup() for this function to work!

Syntax

dimmer.mainsFrequency()

Returns

Integer with the measured mains frequency, in cHz. 0 if mains is not detected.

Example

dimmer.setup(14) -- Zero crossing detector connected to GPIO14
tmr.create():alarm(1000, tmr.ALARM_AUTO, function()
    print("Mains frequency is %d Hz", dimmer.mainsFrequency() / 100)
end)


These modules come with a TRIAC whose gate is intended to be driven by a GPIO pin, isolated from mains by an optocoupler. These modules also come with a zero-crossing detector, also isolated, that raises a pin voltage when the AC mains sine wave signal crosses 0V.

## Architecture of this nodemcu module
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
## Architecture of this nodemcu module
## Architecture of this NodeMCU module

@marcelstoer
Copy link
Member

marcelstoer commented Mar 29, 2020

I'm currently compiling the firmware to start testing.

🥇 The documentation is fantastic. Thank you.

What is the reason you do not allow setting the dimmer pins in setup()? It's certainly nice that I can dynamically add and remove pins for dimming. However, intuitively I expected to have to define those pins during setup. Also, how about making the pin in setLevel() optional?

```lua
dimmer.setup(14) -- configure pin 14 as zero-crossing detector
dimmer.add(22) -- TRIAC gate controlling lightbulb is connected to GPIO pin 22
dimmer.setLevel(300) -- set brightness to 300‰ (30%)
Copy link
Member

@marcelstoer marcelstoer Mar 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
dimmer.setLevel(300) -- set brightness to 300‰ (30%)
dimmer.setLevel(22, 300) -- set brightness to 300‰ (30%)

Comment on lines +30 to +32
* Enable *FreeRTOS in both cores* by unselecting *"Component Config/FreeRTOS/Run FreeRTOS only on first core"*. This will allow the module to use CPU1.
* Unselect *"Component Config/ESP32-specific/Also watch CPU1 tick interrupt"*
* Unselect *"Component Config/ESP32-specific/Watch CPU1 idle task"*
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Enable *FreeRTOS in both cores* by unselecting *"Component Config/FreeRTOS/Run FreeRTOS only on first core"*. This will allow the module to use CPU1.
* Unselect *"Component Config/ESP32-specific/Also watch CPU1 tick interrupt"*
* Unselect *"Component Config/ESP32-specific/Watch CPU1 idle task"*
* Enable FreeRTOS in both cores by **deselecting** `Component Config` > `FreeRTOS` > `Run FreeRTOS only on first core`. This will allow the module to also use CPU1 (besides CPU0).
* **Deselect** `Component Config` > `ESP32-specific` > `Also watch CPU1 tick interrupt`
* **Deselect** `Component Config` > `ESP32-specific` > `Watch CPU1 idle task`

```lua
dimmer.setup(14) -- Zero crossing detector connected to GPIO14
tmr.create():alarm(1000, tmr.ALARM_AUTO, function()
print("Mains frequency is %d Hz", dimmer.mainsFrequency() / 100)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prints Mains frequency is %d Hz 49.95 here.


## dimmer.remove()

Removes the given pin from the dimmer module
Copy link
Member

@marcelstoer marcelstoer Mar 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it intentional that this does not set the level back to 0 i.e. the light remains on? Intuitively I was expecting it to get turned off.

@stale
Copy link

stale bot commented Aug 28, 2021

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the stale label Aug 28, 2021
@stale stale bot closed this Apr 16, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants