Skip to content

Commit b516807

Browse files
committed
feat: support non-PWM tacho pins
refactor: handle all tacho pins w/ 1 task
1 parent 3cb1475 commit b516807

File tree

4 files changed

+133
-65
lines changed

4 files changed

+133
-65
lines changed

src/config/pins.cpp

+19-10
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,11 @@
11
#include "pins.hpp"
2-
#include "config.hpp"
32
#include "hardware/gpio.h"
43
#include "hardware/i2c.h"
54
#include "hardware/spi.h"
5+
#include "utility/square_wave.hpp"
66
#include <cassert>
77
#include <cstdint>
88

9-
#ifndef NDEBUG
10-
#include "utility/square_wave.hpp"
11-
#endif
12-
139
namespace nevermore {
1410

1511
namespace {
@@ -111,13 +107,26 @@ bool Pins::apply() const {
111107
bind_bus(i2c, bind_i2c_hw, bind_i2c_pio);
112108
bind_bus(spi, bind_spi_hw, bind_spi_pio);
113109

114-
foreach_pwm_function([](auto&& pins, bool) {
115-
for (auto&& pin : pins)
116-
if (pin) gpio_set_function(pin, GPIO_FUNC_PWM);
110+
uint32_t pwm_slice_claimed = 0;
111+
foreach_pwm_function([&](auto&& pins, bool) {
112+
for (auto&& pin : pins) {
113+
if (!pin) continue;
114+
115+
gpio_set_function(pin, GPIO_FUNC_PWM);
116+
pwm_slice_claimed |= 1u << pwm_gpio_to_slice_num_(pin);
117+
}
117118
});
118119

119-
for (auto&& pin : fan_tachometer)
120-
if (pin) gpio_pull_up(pin);
120+
for (auto&& pin : fan_tachometer) {
121+
if (!pin) continue;
122+
123+
auto slice = pwm_gpio_to_channel(pin) == PWM_CHAN_B ? 1u << pwm_gpio_to_slice_num_(pin) : 0;
124+
// FUTURE WORK: remove dbg msg once this feature matures
125+
printf("tacho pin=%d mode=%s\n", (int)pin, !slice || pwm_slice_claimed & slice ? "POLL" : "PWM");
126+
gpio_set_function(pin, !slice || pwm_slice_claimed & slice ? GPIO_FUNC_SIO : GPIO_FUNC_PWM);
127+
gpio_pull_up(pin);
128+
pwm_slice_claimed |= slice;
129+
}
121130

122131
// we're setting up the WS2812 controller on PIO0
123132
for (auto&& pin : neopixel_data)

src/config/pins.hpp

-5
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,6 @@ struct [[gnu::packed]] Pins {
175175
constexpr void validate_or_throw() const;
176176

177177
constexpr void foreach_pwm_function(auto&& go) const {
178-
go(fan_tachometer, false);
179178
go(fan_pwm, true);
180179
go(photocatalytic_pwm, true);
181180
// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
@@ -346,10 +345,6 @@ inline constexpr void nevermore::Pins::validate_or_throw() const {
346345
display_buses += bus && bus.kind == BusSPI::Kind::display;
347346
if (1 < display_buses) throw "Config uses multiple display SPI buses.";
348347

349-
for (auto&& pin : fan_tachometer)
350-
if (pin && pwm_gpio_to_channel_(pin) != PWM_CHAN_B)
351-
throw "Config uses fan tachometer on slice A GPIO. Put it on a B GPIO (i.e odd GPIO).";
352-
353348
uint32_t pwm_slice_claimed = 0;
354349
foreach_pwm_function([&](std::span<GPIO const> pins, bool allow_sharing) {
355350
for (auto&& pin : pins) {

src/gatt/fan.cpp

+4-20
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ constexpr uint32_t FAN_PWN_HZ = 25'000;
4444

4545
BLE::Percentage8 g_fan_power = 0;
4646
BLE::Percentage8 g_fan_power_override; // not-known -> automatic control
47-
array<sensors::Tachometer, Pins{}.fan_tachometer.size()> g_tachometers;
47+
sensors::Tachometer g_tachometer;
4848

4949
struct [[gnu::packed]] FanPowerTachoAggregate {
5050
BLE::Percentage8 power = g_fan_power;
@@ -93,11 +93,7 @@ void fan_power_set(BLE::Percentage8 power, sensors::Sensors const& sensors = sen
9393
} // namespace
9494

9595
double fan_rpm() {
96-
double total = 0;
97-
for (auto const& t : g_tachometers)
98-
total += t.revolutions_per_second() * 60;
99-
100-
return total;
96+
return g_tachometer.revolutions_per_second() * 60;
10197
}
10298

10399
double fan_power() {
@@ -129,24 +125,12 @@ bool init() {
129125
pwm_init(pwm_gpio_to_slice_num_(pin), &cfg, true);
130126
}
131127

132-
auto* it_tacho = begin(g_tachometers);
133-
for (auto&& pin : Pins::active().fan_tachometer) {
134-
if (!pin) continue;
135-
assert(it_tacho != end(g_tachometers));
136-
it_tacho->setup(pin, TACHOMETER_PULSE_PER_REVOLUTION);
137-
it_tacho++; // NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic)
138-
139-
auto cfg = pwm_get_default_config();
140-
pwm_config_set_clkdiv_mode(&cfg, PWM_DIV_B_FALLING);
141-
pwm_init(pwm_gpio_to_slice_num_(pin), &cfg, false);
142-
}
128+
g_tachometer.setup(Pins::active().fan_tachometer, TACHOMETER_PULSE_PER_REVOLUTION);
129+
g_tachometer.start();
143130

144131
// set fan PWM level
145132
fan_power_set(g_fan_power);
146133

147-
for (auto& t : g_tachometers)
148-
if (t.pin()) t.start();
149-
150134
// HACK: We'd like to notify on write to tachometer changes, but the code base isn't setup
151135
// for that yet. Internally poll and update based on diffs for now.
152136
mk_timer("gatt-fan-tachometer-notify", SENSOR_UPDATE_PERIOD)([](auto*) {

src/sensors/tachometer.hpp

+110-30
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,50 @@
22

33
#include "async_sensor.hpp"
44
#include "config/pins.hpp"
5+
#include "hardware/gpio.h"
56
#include "sdk/pwm.hpp"
7+
#include <algorithm>
8+
#include <bitset>
69
#include <chrono>
710
#include <cstdint>
811

912
namespace nevermore::sensors {
1013

1114
using namespace std::literals::chrono_literals;
1215

16+
// 'Low' speed tachometer, intended for < 1000 pulses/sec.
17+
// DELTA BFB0712H spec sheet says an RPM of 2900 -> ~49 rev/s
18+
// PWM counter wraps at 2^16-1 -> if a fan is spinning that fast you've a problem.
1319
struct Tachometer final : SensorPeriodic {
14-
// DELTA BFB0712H spec sheet says an RPM of 2900 -> ~49 rev/s
15-
// Honestly at this low a rate it might be worth just using
16-
// an interrupt instead of screwing with a PWM slice...
17-
constexpr static auto TACHOMETER_READ_PERIOD = SENSOR_UPDATE_PERIOD;
18-
static_assert(100ms <= TACHOMETER_READ_PERIOD && "need at least 100ms to get a good sampling");
19-
20-
Tachometer(GPIO pin = GPIO::none(), uint32_t pulses_per_revolution = 1) {
21-
setup(pin, pulses_per_revolution);
22-
}
20+
// need at least 100ms for a reasonable read and no point sampling longer than 1s
21+
constexpr static auto TACHOMETER_READ_PERIOD =
22+
std::clamp<std::chrono::milliseconds>(SENSOR_UPDATE_PERIOD, 100ms, 1s);
23+
24+
// hz_max_pulse = hz_sample / 2
25+
// sample at 10 kHz, that'll support up to 150'000 RPM w/ 2 pulses per rev
26+
constexpr static auto PIN_SAMPLING_PERIOD = 0.1ms;
27+
28+
Tachometer() = default;
2329

24-
void setup(GPIO pin, uint32_t pulses_per_revolution = 1) {
25-
assert((!pin || pwm_gpio_to_channel(pin) == PWM_CHAN_B) && "tachometers must run on a B channel");
30+
void setup(Pins::GPIOs const& pins, uint32_t pulses_per_revolution = 1) {
2631
assert(0 < pulses_per_revolution);
27-
this->pin_ = pin;
28-
this->pulses_per_revolution = pulses_per_revolution;
29-
}
3032

31-
[[nodiscard]] GPIO pin() const {
32-
return pin_;
33+
for (auto&& pin : pins) {
34+
if (!pin) continue;
35+
36+
switch (gpio_get_function(pin)) {
37+
default: assert(false); break;
38+
case GPIO_FUNC_SIO: break;
39+
case GPIO_FUNC_PWM: {
40+
auto cfg = pwm_get_default_config();
41+
pwm_config_set_clkdiv_mode(&cfg, PWM_DIV_B_FALLING);
42+
pwm_init(pwm_gpio_to_slice_num_(pin), &cfg, false);
43+
} break;
44+
}
45+
}
46+
47+
std::copy(std::begin(pins), std::end(pins), this->pins);
48+
this->pulses_per_revolution = pulses_per_revolution;
3349
}
3450

3551
[[nodiscard]] double revolutions_per_second() const {
@@ -42,29 +58,93 @@ struct Tachometer final : SensorPeriodic {
4258

4359
protected:
4460
void read() override {
45-
if (!pin_) return;
46-
auto slice_num = pwm_gpio_to_slice_num_(pin_);
47-
48-
pwm_set_counter(slice_num, 0);
61+
// Since we're targetting relatively low pulse hz don't bother about
62+
// keeping code in RAM to avoid flash penalty or function overhead;
63+
// we're running at 125 MHz & reading <= 1 kHz pulse signals,
64+
// we've plenty of room for sloppiness.
4965
auto begin = std::chrono::steady_clock::now();
50-
pwm_set_enabled(slice_num, true);
51-
52-
task_delay(TACHOMETER_READ_PERIOD);
53-
54-
pwm_set_enabled(slice_num, false);
66+
uint32_t pulses = pulse_count(begin);
5567
auto end = std::chrono::steady_clock::now();
5668

5769
auto duration_sec =
5870
std::chrono::duration_cast<std::chrono::duration<double, std::ratio<1>>>(end - begin);
59-
auto count = pwm_get_counter(slice_num);
60-
revolutions_per_second_ = count / duration_sec.count() / pulses_per_revolution;
71+
revolutions_per_second_ = pulses / duration_sec.count() / pulses_per_revolution;
6172

62-
// printf("tachometer_measure dur=%f s cnt=%d rev-per-sec=%f rpm=%f\n",
63-
// duration_sec.count(), int(count), revolutions_per_second_, revolutions_per_second_ * 60);
73+
// printf("tachometer_measure dur=%f s cnt=%u rev-per-sec=%f rpm=%f\n", duration_sec.count(),
74+
// unsigned(pulses), revolutions_per_second_, revolutions_per_second_ * 60);
6475
}
6576

6677
private:
67-
GPIO pin_;
78+
// we're nowhere near high precision stuff
79+
uint32_t pulse_count(std::chrono::steady_clock::time_point const begin) {
80+
uint32_t pulses = 0;
81+
if (pulse_start()) { // polling required, we've non-PWM tacho pins
82+
for (auto now = begin; (now - begin) < TACHOMETER_READ_PERIOD;
83+
now = std::chrono::steady_clock::now()) {
84+
pulses += pulse_poll();
85+
task_delay(PIN_SAMPLING_PERIOD);
86+
}
87+
} else // everything is handled by PWM slices, just nap for a bit
88+
task_delay(TACHOMETER_READ_PERIOD);
89+
90+
pulses += pulse_end(); // add pulses from PWM counters
91+
return pulses;
92+
}
93+
94+
bool pulse_start() {
95+
bool do_polling = false;
96+
97+
for (auto&& pin : pins) {
98+
if (!pin) continue;
99+
100+
switch (gpio_get_function(pin)) {
101+
default: break;
102+
case GPIO_FUNC_SIO: {
103+
do_polling = true;
104+
state.set(&pin - pins, gpio_get(pin));
105+
} break;
106+
case GPIO_FUNC_PWM: {
107+
auto slice = pwm_gpio_to_slice_num_(pin);
108+
pwm_set_counter(slice, 0);
109+
pwm_set_enabled(slice, true);
110+
} break;
111+
}
112+
}
113+
114+
return do_polling;
115+
}
116+
117+
uint32_t pulse_poll() {
118+
uint32_t pulses = 0;
119+
120+
for (auto&& pin : pins) {
121+
if (pin && gpio_get_function(pin) == GPIO_FUNC_SIO) {
122+
auto curr = gpio_get(pin);
123+
auto prev = state.test(&pin - pins);
124+
pulses += (prev != curr) && curr; // count rising edges
125+
state.set(&pin - pins, curr);
126+
}
127+
}
128+
129+
return pulses;
130+
}
131+
132+
uint32_t pulse_end() {
133+
uint32_t pulses = 0;
134+
135+
for (auto&& pin : pins) {
136+
if (pin && gpio_get_function(pin) == GPIO_FUNC_PWM) {
137+
auto slice = pwm_gpio_to_slice_num_(pin);
138+
pwm_set_enabled(slice, false);
139+
pulses += pwm_get_counter(slice);
140+
}
141+
}
142+
143+
return pulses;
144+
}
145+
146+
Pins::GPIOs pins;
147+
std::bitset<Pins::ALTERNATIVES_MAX> state;
68148
uint pulses_per_revolution = 1;
69149
double revolutions_per_second_ = 0;
70150
};

0 commit comments

Comments
 (0)