-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathled.hpp
245 lines (225 loc) · 9.02 KB
/
led.hpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
#pragma once
#include <algorithm>
#include <cmath>
#include <optional>
#include <vector>
#include <driver/ledc.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include "logger.hpp"
namespace espp {
/**
* Provides a wrapper around the LEDC peripheral in ESP-IDF which allows for
* thread-safe control over one or more channels of LEDs using a simpler API.
*
* \section led_ex1 Linear LED Example
* \snippet led_example.cpp linear led example
* \section led_ex2 Breathing LED Example
* \snippet led_example.cpp breathing led example
*/
class Led {
public:
/**
* Represents one LED channel.
*/
struct ChannelConfig {
size_t gpio; /**< The GPIO pin the LED is connected to. */
ledc_channel_t channel; /**< The LEDC channel that you want associated with this LED. */
ledc_timer_t timer; /**< The LEDC timer that you want associated with this LED channel. */
float duty{
0}; /**< The starting duty cycle (%) [0, 100] that you want the LED channel to have. */
ledc_mode_t speed_mode{
LEDC_LOW_SPEED_MODE}; /**< The LEDC speed mode you want for this LED channel. */
bool output_invert{false}; /**< Whether to invert the GPIO output for this LED channel. */
};
/**
* Configuration Struct for the LEDC subsystem including the different LED
* channels that should be associated.
*/
struct Config {
ledc_timer_t timer; /**< The LEDC timer that you want associated with the LEDs. */
size_t frequency_hz; /**< The frequency that you want to run the PWM hardawre for the LEDs at.
@note this is inversely related to the duty resolution configuration. */
std::vector<ChannelConfig> channels; /**< The LED channels that you want to control. */
ledc_timer_bit_t duty_resolution{
LEDC_TIMER_13_BIT}; /**< The resolution of the duty cycle for these LEDs. @note this is
inversely related to the frequency configuration. */
ledc_mode_t speed_mode{
LEDC_LOW_SPEED_MODE}; /**< The LEDC speed mode you want for these LED channels. */
Logger::Verbosity log_level{Logger::Verbosity::WARN}; /**< Log verbosity for the task. */
};
/**
* @brief Initialize the LEDC subsystem according to the configuration.
* @param config The configuration structure for the LEDC subsystem.
*/
explicit Led(const Config &config)
: duty_resolution_(config.duty_resolution),
max_raw_duty_((uint32_t)(std::pow(2, (int)duty_resolution_) - 1)),
channels_(config.channels), logger_({.tag = "Led", .level = config.log_level}) {
logger_.info("Initializing timer");
ledc_timer_config_t ledc_timer;
memset(&ledc_timer, 0, sizeof(ledc_timer));
ledc_timer.duty_resolution = duty_resolution_;
ledc_timer.freq_hz = config.frequency_hz;
ledc_timer.speed_mode = config.speed_mode;
ledc_timer.timer_num = config.timer;
ledc_timer.clk_cfg = LEDC_AUTO_CLK;
ledc_timer_config(&ledc_timer);
logger_.info("Initializing channels");
for (auto conf : channels_) {
uint32_t actual_duty = std::clamp(conf.duty, 0.0f, 100.0f) * max_raw_duty_ / 100.0f;
ledc_channel_config_t channel_conf;
memset(&channel_conf, 0, sizeof(channel_conf));
channel_conf.channel = conf.channel;
channel_conf.duty = actual_duty;
channel_conf.gpio_num = conf.gpio;
channel_conf.speed_mode = conf.speed_mode;
channel_conf.hpoint = 0;
channel_conf.timer_sel = conf.timer;
channel_conf.flags.output_invert = conf.output_invert;
ledc_channel_config(&channel_conf);
}
logger_.info("Initializing the fade service");
ledc_fade_func_install(0);
ledc_cbs_t callbacks = {.fade_cb = &Led::cb_ledc_fade_end_event};
// we associate each channel with its own semaphore so that they can be
// faded / controlled independently.
logger_.info("Creating semaphores");
fade_semaphores_.resize(channels_.size());
for (auto &sem : fade_semaphores_) {
sem = xSemaphoreCreateBinary();
// go ahead and give to the semaphores so the functions will work
xSemaphoreGive(sem);
}
for (int i = 0; i < channels_.size(); i++) {
ledc_cb_register(channels_[i].speed_mode, channels_[i].channel, &callbacks,
(void *)fade_semaphores_[i]);
}
}
/**
* @brief Stop the LEDC subsystem and free memory.
*/
~Led() {
// clean up the semaphores
for (auto &sem : fade_semaphores_) {
// take the semaphore (so that we don't delete it until no one is
// blocked on it)
xSemaphoreTake(sem, portMAX_DELAY);
// and delete it
vSemaphoreDelete(sem);
}
ledc_fade_func_uninstall();
}
/**
* @brief Can the LED settings can be changed for the channel? If this
* function returns true, then (threaded race conditions aside), the
* set_duty() and set_fade_with_time() functions should not block.
* @param channel The channel to check
* @return True if the channel settings can be changed, false otherwise
*/
bool can_change(ledc_channel_t channel) {
int index = get_channel_index(channel);
if (index == -1) {
return false;
}
auto &sem = fade_semaphores_[index];
return uxSemaphoreGetCount(sem) == 1;
}
/**
* @brief Get the current duty cycle this channel has.
* @param channel The channel in question
* @return The duty percentage [0.0f, 100.0f] if the channel is managed,
* std::nullopt otherwise
*/
std::optional<float> get_duty(ledc_channel_t channel) const {
int index = get_channel_index(channel);
if (index == -1) {
return {};
}
const auto &conf = channels_[index];
auto raw_duty = ledc_get_duty(conf.speed_mode, conf.channel);
return (float)raw_duty / (float)max_raw_duty_ * 100.0f;
}
/**
* @brief Set the duty cycle for this channel.
* @note This function will block until until a current fade process
* completes (if there is one).
* @param channel The channel to set the duty cycle for.
* @param duty_percent The new duty percentage, [0.0, 100.0].
*/
void set_duty(ledc_channel_t channel, float duty_percent) {
int index = get_channel_index(channel);
if (index == -1) {
return;
}
auto conf = channels_[index];
auto &sem = fade_semaphores_[index];
// ensure that it's not fading if it is
xSemaphoreTake(sem, portMAX_DELAY);
uint32_t actual_duty = std::clamp(duty_percent, 0.0f, 100.0f) * max_raw_duty_ / 100.0f;
ledc_set_duty(conf.speed_mode, conf.channel, actual_duty);
ledc_update_duty(conf.speed_mode, conf.channel);
// make sure others can set this channel now as well
xSemaphoreGive(sem);
}
/**
* @brief Set the duty cycle for this channel, fading from the current duty
* cycle to the new duty cycle over \p fade_time_ms milliseconds.
* @note This function will block until until a current fade process
* completes (if there is one).
* @param channel The channel to fade.
* @param duty_percent The new duty percentage to fade to, [0.0, 100.0].
* @param fade_time_ms The number of milliseconds for which to fade.
*/
void set_fade_with_time(ledc_channel_t channel, float duty_percent, uint32_t fade_time_ms) {
int index = get_channel_index(channel);
if (index == -1) {
return;
}
auto conf = channels_[index];
auto &sem = fade_semaphores_[index];
// ensure that it's not fading if it is
xSemaphoreTake(sem, portMAX_DELAY);
uint32_t actual_duty = std::clamp(duty_percent, 0.0f, 100.0f) * max_raw_duty_ / 100.0f;
ledc_set_fade_with_time(conf.speed_mode, conf.channel, actual_duty, fade_time_ms);
ledc_fade_start(conf.speed_mode, conf.channel, LEDC_FADE_NO_WAIT);
// NOTE: we don't give the semaphore back here because that is the job of
// the ISR
}
protected:
/**
* @brief Get the index of channel in channels_, -1 if not found.
* @note We implement this instead of using std::find because we cannot use
* an iterator efficiently with the semaphore array which we need to
* do.
* @param channel Channel to find.
* @return -1 if not found, index of channel if found.
*/
int get_channel_index(ledc_channel_t channel) const {
for (int i = 0; i < channels_.size(); i++) {
if (channels_[i].channel == channel) {
return i;
}
}
return -1;
}
/**
* This callback function will be called when fade operation has ended
* Use callback only if you are aware it is being called inside an ISR
* Otherwise, you can use a semaphore to unblock tasks
*/
static bool IRAM_ATTR cb_ledc_fade_end_event(const ledc_cb_param_t *param, void *user_arg) {
portBASE_TYPE taskAwoken = pdFALSE;
if (param->event == LEDC_FADE_END_EVT) {
SemaphoreHandle_t sem = (SemaphoreHandle_t)user_arg;
xSemaphoreGiveFromISR(sem, &taskAwoken);
}
return (taskAwoken == pdTRUE);
}
ledc_timer_bit_t duty_resolution_;
uint32_t max_raw_duty_;
std::vector<SemaphoreHandle_t> fade_semaphores_;
std::vector<ChannelConfig> channels_;
Logger logger_;
};
} // namespace espp