CDC Badge OS
Firmware for the CDC Badge v1.0 hardware security key
Loading...
Searching...
No Matches
host_api_gpio.cpp
Go to the documentation of this file.
1
14
15#include "cdc_hal/hw_config.h"
16#include "cdc_hal/II2cBus.h"
20
21#include "driver/gpio.h"
22#include "driver/ledc.h"
23#include "esp_adc/adc_oneshot.h"
24#include "cdc_log.h"
25
26#include <array>
27#include <cstring>
28
29extern "C" void* plg_get_active_plugin(void);
30extern "C" void plg_log_warn(const char* msg);
31
32namespace {
33
34constexpr const char* TAG = "plg_gpio";
35
36struct PinLock {
37 void* owner = nullptr;
38 bool in_use = false;
39};
40constexpr size_t MAX_GPIO_PINS = 49;
41std::array<PinLock, MAX_GPIO_PINS> s_pin_locks{};
42
45}
46
47bool manifest_allows_gpio(uint8_t pin) {
48 auto* p = active();
49 if (!p) return false;
50 const auto& cap = p->manifest().capabilities;
51 if (cap.grove && (pin == 2 || pin == 3)) return true;
52 if (cap.sao && (pin == 15 || pin == 16)) return true;
53 for (uint8_t allowed : cap.gpio_pins) if (allowed == pin) return true;
54 for (uint8_t allowed : cap.pwm_pins) if (allowed == pin) return true;
55 for (uint8_t allowed : cap.adc_pins) if (allowed == pin) return true;
56 return false;
57}
58
59int acquire_lock(uint8_t pin) {
60 if (pin >= MAX_GPIO_PINS) return HOST_ERR_INVALID_ARG;
62 if (!manifest_allows_gpio(pin)) return HOST_ERR_NO_CAPABILITY;
63
64 auto* a = active();
65 PinLock& lock = s_pin_locks[pin];
66 if (lock.in_use && lock.owner != a) return HOST_ERR_BUSY;
67 lock.owner = a;
68 lock.in_use = true;
69 return HOST_OK;
70}
71
72void release_lock(uint8_t pin) {
73 if (pin >= MAX_GPIO_PINS) return;
74 s_pin_locks[pin] = PinLock{};
75}
76
77// LEDC low-speed channel allocation: each PWM pin gets its own channel so duties
78// are independent. All channels share LEDC_TIMER_0 and therefore the frequency
79// set by the first host_gpio_pwm_start call.
80struct PwmChannel {
81 uint8_t pin = 0;
82 bool used = false;
83 void* owner = nullptr;
84};
85std::array<PwmChannel, LEDC_CHANNEL_MAX> s_pwm_channels{};
86
87int pwm_channel_for(uint8_t pin) {
88 for (size_t i = 0; i < s_pwm_channels.size(); ++i)
89 if (s_pwm_channels[i].used && s_pwm_channels[i].pin == pin)
90 return static_cast<int>(i);
91 return -1;
92}
93
94int pwm_channel_alloc(uint8_t pin) {
95 for (size_t i = 0; i < s_pwm_channels.size(); ++i) {
96 if (!s_pwm_channels[i].used) {
97 s_pwm_channels[i] = { pin, true, active() };
98 return static_cast<int>(i);
99 }
100 }
101 return -1;
102}
103
104// Only the expansion bus (I2C1) is reachable by plugins. Bus 0 carries the
105// charger (BQ25895) and IO expander (TCA9535) and is never exposed. The
106// TROPIC01 is on SPI, not I2C, so it is unreachable here by design.
107bool manifest_allows_i2c(uint8_t bus) {
108 auto* p = active();
109 if (!p) return false;
110 for (uint8_t b : p->manifest().capabilities.i2c_bus) if (b == bus) return true;
111 return false;
112}
113
114// Fetch the expansion bus, ensuring its driver is installed (idempotent).
115cdc::hal::II2cBus* expansion_bus() {
116 auto* bus = cdc::hal::getI2cBus1();
117 return (bus && bus->init()) ? bus : nullptr;
118}
119
120} // namespace
121
122extern "C" {
123
124int host_gpio_set_direction(uint8_t pin, uint8_t direction)
125{
126 int rc = acquire_lock(pin);
127 if (rc != HOST_OK) return rc;
128
129 gpio_config_t cfg{};
130 cfg.pin_bit_mask = 1ULL << pin;
131 cfg.intr_type = GPIO_INTR_DISABLE;
132 cfg.pull_up_en = GPIO_PULLUP_DISABLE;
133 cfg.pull_down_en = GPIO_PULLDOWN_DISABLE;
134 switch (direction) {
135 case GPIO_DIR_IN: cfg.mode = GPIO_MODE_INPUT; break;
136 case GPIO_DIR_OUT: cfg.mode = GPIO_MODE_OUTPUT; break;
137 case GPIO_DIR_OUT_OD: cfg.mode = GPIO_MODE_OUTPUT_OD; break;
138 default: return HOST_ERR_INVALID_ARG;
139 }
140 return gpio_config(&cfg) == ESP_OK ? HOST_OK : HOST_ERR_GENERIC;
141}
142
143int host_gpio_set_pull(uint8_t pin, uint8_t pull)
144{
145 int rc = acquire_lock(pin);
146 if (rc != HOST_OK) return rc;
147
148 gpio_pull_mode_t mode = GPIO_FLOATING;
149 switch (pull) {
150 case GPIO_PULL_UP: mode = GPIO_PULLUP_ONLY; break;
151 case GPIO_PULL_DOWN: mode = GPIO_PULLDOWN_ONLY; break;
152 case GPIO_PULL_NONE: mode = GPIO_FLOATING; break;
153 default: return HOST_ERR_INVALID_ARG;
154 }
155 return gpio_set_pull_mode(static_cast<gpio_num_t>(pin), mode) == ESP_OK
157}
158
159int host_gpio_write(uint8_t pin, bool level)
160{
161 int rc = acquire_lock(pin);
162 if (rc != HOST_OK) return rc;
163 return gpio_set_level(static_cast<gpio_num_t>(pin), level ? 1 : 0) == ESP_OK
165}
166
167int host_gpio_read(uint8_t pin, bool* level)
168{
169 if (!level) return HOST_ERR_INVALID_ARG;
170 int rc = acquire_lock(pin);
171 if (rc != HOST_OK) return rc;
172 *level = gpio_get_level(static_cast<gpio_num_t>(pin)) != 0;
173 return HOST_OK;
174}
175
176int host_gpio_release(uint8_t pin)
177{
178 if (pin >= MAX_GPIO_PINS) return HOST_ERR_INVALID_ARG;
179 // Only the owner can release; silently ignore alien plugins.
180 if (s_pin_locks[pin].owner != active()) return HOST_OK;
181 gpio_reset_pin(static_cast<gpio_num_t>(pin));
182 release_lock(pin);
183 return HOST_OK;
184}
185
186void plg_gpio_on_unload(void* plugin)
187{
188 if (!plugin) return;
189 for (uint8_t pin = 0; pin < MAX_GPIO_PINS; ++pin) {
190 if (!s_pin_locks[pin].in_use || s_pin_locks[pin].owner != plugin) continue;
191 int ch = pwm_channel_for(pin);
192 if (ch >= 0) {
193 ledc_stop(LEDC_LOW_SPEED_MODE, static_cast<ledc_channel_t>(ch), 0);
194 s_pwm_channels[ch] = PwmChannel{};
195 LOG_W(TAG, "force-releasing leaked PWM on GPIO %u", pin);
196 }
197 gpio_reset_pin(static_cast<gpio_num_t>(pin));
198 release_lock(pin);
199 LOG_W(TAG, "force-releasing leaked GPIO %u", pin);
200 }
201 // Sweep PWM channels too, in case one outlived its pin lock.
202 for (size_t i = 0; i < s_pwm_channels.size(); ++i) {
203 if (!s_pwm_channels[i].used || s_pwm_channels[i].owner != plugin) continue;
204 ledc_stop(LEDC_LOW_SPEED_MODE, static_cast<ledc_channel_t>(i), 0);
205 LOG_W(TAG, "force-releasing leaked PWM channel %u", static_cast<unsigned>(i));
206 s_pwm_channels[i] = PwmChannel{};
207 }
208}
209
210int host_gpio_pwm_start(uint8_t pin, uint32_t freq_hz, uint16_t duty_per_mille)
211{
212 int rc = acquire_lock(pin);
213 if (rc != HOST_OK) return rc;
214 if (duty_per_mille > 1000) return HOST_ERR_INVALID_ARG;
215
216 int ch = pwm_channel_for(pin);
217 const bool fresh = (ch < 0);
218 if (fresh) ch = pwm_channel_alloc(pin);
219 if (ch < 0) return HOST_ERR_NO_MEMORY;
220
221 static bool s_timer_inited = false;
222 if (!s_timer_inited) {
223 ledc_timer_config_t timer{};
224 timer.speed_mode = LEDC_LOW_SPEED_MODE;
225 timer.duty_resolution = LEDC_TIMER_10_BIT;
226 timer.timer_num = LEDC_TIMER_0;
227 timer.freq_hz = freq_hz ? freq_hz : 5000;
228 timer.clk_cfg = LEDC_AUTO_CLK;
229 if (ledc_timer_config(&timer) != ESP_OK) {
230 if (fresh) s_pwm_channels[ch] = PwmChannel{};
231 return HOST_ERR_GENERIC;
232 }
233 s_timer_inited = true;
234 }
235
236 ledc_channel_config_t cfg{};
237 cfg.channel = static_cast<ledc_channel_t>(ch);
238 cfg.duty = (1023 * duty_per_mille) / 1000;
239 cfg.gpio_num = pin;
240 cfg.speed_mode = LEDC_LOW_SPEED_MODE;
241 cfg.hpoint = 0;
242 cfg.timer_sel = LEDC_TIMER_0;
243 if (ledc_channel_config(&cfg) != ESP_OK) {
244 if (fresh) s_pwm_channels[ch] = PwmChannel{};
245 return HOST_ERR_GENERIC;
246 }
247 return HOST_OK;
248}
249
250int host_gpio_pwm_set_duty(uint8_t pin, uint16_t duty_per_mille)
251{
252 if (duty_per_mille > 1000) return HOST_ERR_INVALID_ARG;
253 int ch = pwm_channel_for(pin);
254 if (ch < 0) return HOST_ERR_INVALID_ARG;
255 auto channel = static_cast<ledc_channel_t>(ch);
256 uint32_t duty = (1023 * duty_per_mille) / 1000;
257 if (ledc_set_duty(LEDC_LOW_SPEED_MODE, channel, duty) != ESP_OK) return HOST_ERR_GENERIC;
258 if (ledc_update_duty(LEDC_LOW_SPEED_MODE, channel) != ESP_OK) return HOST_ERR_GENERIC;
259 return HOST_OK;
260}
261
262int host_gpio_pwm_stop(uint8_t pin)
263{
264 int ch = pwm_channel_for(pin);
265 if (ch < 0) return HOST_ERR_INVALID_ARG;
266 ledc_stop(LEDC_LOW_SPEED_MODE, static_cast<ledc_channel_t>(ch), 0);
267 s_pwm_channels[ch] = PwmChannel{};
268 return HOST_OK;
269}
270
271int host_adc_read(uint8_t pin, uint16_t* raw, uint16_t* millivolt)
272{
273 if (!raw && !millivolt) return HOST_ERR_INVALID_ARG;
274 int rc = acquire_lock(pin);
275 if (rc != HOST_OK) return rc;
276
277 // ADC1 channel mapping for user pins (ADC2 conflicts with WiFi).
278 struct AdcMap { uint8_t pin; adc_channel_t ch; };
279 constexpr AdcMap PINS[] = {
280 { 2, ADC_CHANNEL_1 }, { 3, ADC_CHANNEL_2 },
281 { 4, ADC_CHANNEL_3 }, { 5, ADC_CHANNEL_4 },
282 { 6, ADC_CHANNEL_5 }, { 7, ADC_CHANNEL_6 },
283 { 9, ADC_CHANNEL_8 },
284 };
285 adc_channel_t ch = static_cast<adc_channel_t>(-1);
286 for (const auto& m : PINS) if (m.pin == pin) ch = m.ch;
287 if (static_cast<int>(ch) < 0) return HOST_ERR_NOT_SUPPORTED;
288
289 adc_oneshot_unit_handle_t unit;
290 adc_oneshot_unit_init_cfg_t unit_cfg{};
291 unit_cfg.unit_id = ADC_UNIT_1;
292 unit_cfg.ulp_mode = ADC_ULP_MODE_DISABLE;
293 if (adc_oneshot_new_unit(&unit_cfg, &unit) != ESP_OK) return HOST_ERR_GENERIC;
294
295 adc_oneshot_chan_cfg_t chan_cfg{};
296 chan_cfg.atten = ADC_ATTEN_DB_12;
297 chan_cfg.bitwidth = ADC_BITWIDTH_DEFAULT;
298 if (adc_oneshot_config_channel(unit, ch, &chan_cfg) != ESP_OK) {
299 adc_oneshot_del_unit(unit);
300 return HOST_ERR_GENERIC;
301 }
302
303 int reading = 0;
304 esp_err_t err = adc_oneshot_read(unit, ch, &reading);
305 adc_oneshot_del_unit(unit);
306 if (err != ESP_OK) return HOST_ERR_GENERIC;
307
308 if (raw) *raw = static_cast<uint16_t>(reading);
309 if (millivolt) {
310 // Rough conversion without calibration; ATTEN_DB_12 gives ~3300 mV full-scale on 12-bit.
311 *millivolt = static_cast<uint16_t>(reading * 3300 / 4095);
312 }
313 return HOST_OK;
314}
315
316// I2C and SAO EEPROM are thin capability gates over the I2cBus HAL; the raw
317// transaction logic lives in cdc_hal/I2cBus, not here.
318
319int host_i2c_write(uint8_t bus, uint8_t addr, const uint8_t* data, size_t len)
320{
321 if (!data && len) return HOST_ERR_INVALID_ARG;
322 if (bus != 1) return HOST_ERR_INVALID_ARG; // only the expansion bus
323 if (!manifest_allows_i2c(bus)) return HOST_ERR_NO_CAPABILITY;
324 auto* b = expansion_bus();
325 if (!b) return HOST_ERR_GENERIC;
326 return b->writeRaw(addr, data, len) == ESP_OK ? HOST_OK : HOST_ERR_GENERIC;
327}
328
329int host_i2c_read(uint8_t bus, uint8_t addr, uint8_t* data, size_t len)
330{
331 if (!data || len == 0) return HOST_ERR_INVALID_ARG;
332 if (bus != 1) return HOST_ERR_INVALID_ARG;
333 if (!manifest_allows_i2c(bus)) return HOST_ERR_NO_CAPABILITY;
334 auto* b = expansion_bus();
335 if (!b) return HOST_ERR_GENERIC;
336 return b->readRaw(addr, data, len) == ESP_OK ? HOST_OK : HOST_ERR_GENERIC;
337}
338
339int host_i2c_write_read(uint8_t bus, uint8_t addr,
340 const uint8_t* wr, size_t wr_len,
341 uint8_t* rd, size_t rd_len)
342{
343 if ((!wr && wr_len) || !rd || rd_len == 0) return HOST_ERR_INVALID_ARG;
344 if (bus != 1) return HOST_ERR_INVALID_ARG;
345 if (!manifest_allows_i2c(bus)) return HOST_ERR_NO_CAPABILITY;
346 auto* b = expansion_bus();
347 if (!b) return HOST_ERR_GENERIC;
348 return b->writeReadRaw(addr, wr, wr_len, rd, rd_len) == ESP_OK ? HOST_OK : HOST_ERR_GENERIC;
349}
350
351int host_i2c_scan(uint8_t bus, uint8_t* found_addrs, size_t* count)
352{
353 if (!found_addrs || !count) return HOST_ERR_INVALID_ARG;
354 if (bus != 1) return HOST_ERR_INVALID_ARG;
355 if (!manifest_allows_i2c(bus)) return HOST_ERR_NO_CAPABILITY;
356 auto* b = expansion_bus();
357 if (!b) return HOST_ERR_GENERIC;
358
359 const size_t cap = *count;
360 size_t n = 0;
361 for (uint8_t addr = 0x08; addr < 0x78 && n < cap; ++addr) {
362 if (b->probe(addr)) found_addrs[n++] = addr;
363 }
364 *count = n;
365 return HOST_OK;
366}
367
368int host_sao_eeprom_read(uint16_t off, uint8_t* buf, size_t len)
369{
370 if (!buf || len == 0) return HOST_ERR_INVALID_ARG;
371 auto* p = active();
372 if (!p || !p->manifest().capabilities.sao) return HOST_ERR_NO_CAPABILITY;
373 auto* b = expansion_bus();
374 if (!b) return HOST_ERR_GENERIC;
375 return b->eepromRead(SAO_EEPROM_ADDR, off, buf, len) == ESP_OK ? HOST_OK : HOST_ERR_GENERIC;
376}
377
378int host_sao_eeprom_write(uint16_t off, const uint8_t* buf, size_t len)
379{
380 if (!buf || len == 0) return HOST_ERR_INVALID_ARG;
381 auto* p = active();
382 if (!p || !p->manifest().capabilities.sao) return HOST_ERR_NO_CAPABILITY;
383 auto* b = expansion_bus();
384 if (!b) return HOST_ERR_GENERIC;
385 return b->eepromWrite(SAO_EEPROM_ADDR, off, buf, len) == ESP_OK ? HOST_OK : HOST_ERR_GENERIC;
386}
387
388} // extern "C"
static const char * TAG
Single source of truth for plugin-accessible GPIO pins.
Owned WAMR module instance + per-plugin state.
CDC Log: logging over TinyUSB CDC and UART.
#define LOG_W(tag, fmt,...)
Definition cdc_log.h:146
#define GPIO_DIR_OUT_OD
Definition host_api.h:1477
int host_i2c_write(uint8_t bus, uint8_t addr, const uint8_t *data, size_t len)
I2C write transaction.
int host_adc_read(uint8_t pin, uint16_t *raw, uint16_t *millivolt)
Single-shot ADC read.
#define GPIO_PULL_NONE
Definition host_api.h:1479
#define GPIO_DIR_OUT
Definition host_api.h:1476
int host_i2c_scan(uint8_t bus, uint8_t *found_addrs, size_t *count)
Scan the I2C bus for responding addresses.
int host_gpio_write(uint8_t pin, bool level)
Drive a digital output high/low.
int host_gpio_set_direction(uint8_t pin, uint8_t direction)
Configure pin direction (one of GPIO_DIR_*).
int host_gpio_set_pull(uint8_t pin, uint8_t pull)
Configure internal pull resistor (one of GPIO_PULL_*).
int host_sao_eeprom_write(uint16_t off, const uint8_t *buf, size_t len)
Write to the SAO addon EEPROM at byte offset.
int host_gpio_pwm_start(uint8_t pin, uint32_t freq_hz, uint16_t duty_per_mille)
Start LEDC PWM on pin.
int host_gpio_read(uint8_t pin, bool *level)
Sample a digital input.
int host_i2c_write_read(uint8_t bus, uint8_t addr, const uint8_t *wr, size_t wr_len, uint8_t *rd, size_t rd_len)
I2C write-then-read transaction with repeated start.
#define GPIO_PULL_UP
Definition host_api.h:1480
int host_gpio_pwm_set_duty(uint8_t pin, uint16_t duty_per_mille)
Update PWM duty without restarting the timer.
int host_gpio_release(uint8_t pin)
Release the pin claim so other plugins can use it.
#define GPIO_DIR_IN
Definition host_api.h:1475
#define GPIO_PULL_DOWN
Definition host_api.h:1481
int host_gpio_pwm_stop(uint8_t pin)
Stop PWM and release the LEDC channel.
int host_sao_eeprom_read(uint16_t off, uint8_t *buf, size_t len)
Read from the SAO addon EEPROM at byte offset.
int host_i2c_read(uint8_t bus, uint8_t addr, uint8_t *data, size_t len)
I2C read transaction.
CDC Badge OS plugin host API - canonical C ABI contract.
#define HOST_ERR_NO_CAPABILITY
Definition host_api.h:40
#define HOST_ERR_NOT_SUPPORTED
Definition host_api.h:45
#define HOST_OK
Definition host_api.h:37
#define HOST_ERR_INVALID_ARG
Definition host_api.h:39
#define HOST_ERR_NO_MEMORY
Definition host_api.h:43
#define HOST_ERR_GENERIC
Definition host_api.h:38
#define HOST_ERR_BUSY
Definition host_api.h:44
void plg_log_warn(const char *msg)
void * plg_get_active_plugin(void)
void plg_gpio_on_unload(void *plugin)
#define SAO_EEPROM_ADDR
Definition hw_config.h:49
II2cBus * getI2cBus1()
Returns singleton instance of I2C bus 1.
Definition I2cBus.cpp:280