Skip to content

Commit 60bd610

Browse files
committed
drivers: input: vs1838b: Add support for VS1838B
The VS1838B is one of the most found infrared receiver found in electronic kits and is easy to setup with only a single GPIO used for signal transmission (apart from VCC and GND). This new driver let applications use the VS1838B as an input with events relayed as 0x0000<address><command>. Only the NEC protocol is supported in this version but more can be added later. Link: https://github-wiki-see.page/m/CoreELEC/remotes/wiki/08.-NEC-IR-Protocol-Datasheet This has been tested using the input_dump sample. Signed-off-by: Bastien JAUNY <[email protected]>
1 parent 6661952 commit 60bd610

File tree

5 files changed

+392
-0
lines changed

5 files changed

+392
-0
lines changed

drivers/input/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ zephyr_library_sources_ifdef(CONFIG_INPUT_SBUS input_sbus.c)
3434
zephyr_library_sources_ifdef(CONFIG_INPUT_STM32_TSC_KEYS input_tsc_keys.c)
3535
zephyr_library_sources_ifdef(CONFIG_INPUT_STMPE811 input_stmpe811.c)
3636
zephyr_library_sources_ifdef(CONFIG_INPUT_TOUCH input_touch.c)
37+
zephyr_library_sources_ifdef(CONFIG_INPUT_VS1838B input_vs1838b.c)
3738
zephyr_library_sources_ifdef(CONFIG_INPUT_XEC_KBD input_xec_kbd.c)
3839
zephyr_library_sources_ifdef(CONFIG_INPUT_XPT2046 input_xpt2046.c)
3940
# zephyr-keep-sorted-stop

drivers/input/Kconfig

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ source "drivers/input/Kconfig.sdl"
3737
source "drivers/input/Kconfig.stmpe811"
3838
source "drivers/input/Kconfig.touch"
3939
source "drivers/input/Kconfig.tsc_keys"
40+
source "drivers/input/Kconfig.vs1838b"
4041
source "drivers/input/Kconfig.xec"
4142
source "drivers/input/Kconfig.xpt2046"
4243
# zephyr-keep-sorted-stop

drivers/input/Kconfig.vs1838b

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Copyright (c) 2025 Bastien Jauny <[email protected]>
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
config INPUT_VS1838B
5+
bool "Vishay VS1838B infrared receiver"
6+
default y
7+
depends on DT_HAS_VISHAY_VS1838B_ENABLED
8+
select GPIO
9+
help
10+
This option enables the driver for the VS1838B infrared receiver.

drivers/input/input_vs1838b.c

+367
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,367 @@
1+
/*
2+
* Copyright (c) 2025 Bastien Jauny <[email protected]>
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
#define DT_DRV_COMPAT vishay_vs1838b
7+
8+
#include <zephyr/device.h>
9+
#include <zephyr/drivers/gpio.h>
10+
#include <zephyr/logging/log.h>
11+
#include <zephyr/input/input.h>
12+
#include <zephyr/kernel.h>
13+
14+
LOG_MODULE_REGISTER(input_vs1838b, CONFIG_INPUT_LOG_LEVEL);
15+
16+
/* A NEC packet is defined by:
17+
* - a lead burst (2 edges)
18+
* - an 8-bit address followed by its logical inverse
19+
* - an 8-bit command followed by its logical inverse
20+
* - a trailing burst
21+
*/
22+
23+
/* Constants used for parsing the edges buffer for NEC protocol */
24+
#define NEC_LEAD_PULSE_EDGE_OFFSET 0
25+
#define NEC_LEAD_PULSE_EDGE_WIDTH 2
26+
27+
#define NEC_ADDRESS_BYTE_EDGE_OFFSET (NEC_LEAD_PULSE_EDGE_OFFSET + NEC_LEAD_PULSE_EDGE_WIDTH)
28+
#define NEC_ADDRESS_BYTE_EDGE_WIDTH (2 * BITS_PER_BYTE)
29+
30+
#define NEC_REVERSE_ADDRESS_BYTE_EDGE_OFFSET \
31+
(NEC_ADDRESS_BYTE_EDGE_OFFSET + NEC_ADDRESS_BYTE_EDGE_WIDTH)
32+
#define NEC_REVERSE_ADDRESS_BYTE_EDGE_WIDTH (2 * BITS_PER_BYTE)
33+
34+
#define NEC_COMMAND_BYTE_EDGE_OFFSET \
35+
(NEC_REVERSE_ADDRESS_BYTE_EDGE_OFFSET + NEC_REVERSE_ADDRESS_BYTE_EDGE_WIDTH)
36+
#define NEC_COMMAND_BYTE_EDGE_WIDTH (2 * BITS_PER_BYTE)
37+
38+
#define NEC_REVERSE_COMMAND_BYTE_EDGE_OFFSET \
39+
(NEC_COMMAND_BYTE_EDGE_OFFSET + NEC_COMMAND_BYTE_EDGE_WIDTH)
40+
#define NEC_REVERSE_COMMAND_BYTE_EDGE_WIDTH (2 * BITS_PER_BYTE)
41+
42+
#define NEC_SINGLE_COMMAND_EDGES_COUNT \
43+
(NEC_REVERSE_COMMAND_BYTE_EDGE_OFFSET + NEC_REVERSE_COMMAND_BYTE_EDGE_WIDTH + 2)
44+
45+
/* NEC protocol values */
46+
#define NEC_LEAD_PULSE_PERIOD_ON_USEC 9000
47+
#define NEC_LEAD_PULSE_PERIOD_OFF_USEC 4500
48+
#define NEC_BIT_DETECT_PERIOD_NSEC 562500
49+
#define NEC_BIT_DETECT_PERIOD_USEC (NEC_BIT_DETECT_PERIOD_NSEC / NSEC_PER_USEC)
50+
#define NEC_BIT_0_TOTAL_PERIOD_USEC 1125
51+
#define NEC_BIT_1_TOTAL_PERIOD_USEC 2250
52+
/* Total delay between a command and a repeat code is 108ms
53+
* and total time of a command is 67.5ms
54+
*/
55+
#define NEC_TIMEOUT_REPEAT_CODE_MSEC (108 - 67)
56+
57+
/* Macros to define tick ranges based on a millisecond tolerance */
58+
#define VS1838B_MIN_TICK(usec, tol) \
59+
((((usec) - (tol)) * CONFIG_SYS_CLOCK_TICKS_PER_SEC) / USEC_PER_SEC)
60+
#define VS1838B_MAX_TICK(usec, tol) \
61+
((((usec) + (tol)) * CONFIG_SYS_CLOCK_TICKS_PER_SEC) / USEC_PER_SEC)
62+
63+
/* Empiric tolerance values. Might be a good idea to put them in the Kconfig? */
64+
#define VS1838B_NEC_LEAD_PULSE_PERIOD_TOLERANCE_USEC 400
65+
#define VS1838B_NEC_BIT_DETECT_PERIOD_TOLERANCE_USEC 150
66+
#define VS1838B_NEC_BIT_0_TOTAL_TOLERANCE_USEC 200
67+
#define VS1838B_NEC_BIT_1_TOTAL_TOLERANCE_USEC 200
68+
69+
/* Tick ranges for the NEC elements */
70+
#define VS1838B_NEC_LEAD_PULSE_ON_MIN_TICK \
71+
VS1838B_MIN_TICK(NEC_LEAD_PULSE_PERIOD_ON_USEC, \
72+
VS1838B_NEC_LEAD_PULSE_PERIOD_TOLERANCE_USEC)
73+
#define VS1838B_NEC_LEAD_PULSE_ON_MAX_TICK \
74+
VS1838B_MAX_TICK(NEC_LEAD_PULSE_PERIOD_ON_USEC, \
75+
VS1838B_NEC_LEAD_PULSE_PERIOD_TOLERANCE_USEC)
76+
77+
#define VS1838B_NEC_LEAD_PULSE_OFF_MIN_TICK \
78+
VS1838B_MIN_TICK(NEC_LEAD_PULSE_PERIOD_OFF_USEC, \
79+
VS1838B_NEC_LEAD_PULSE_PERIOD_TOLERANCE_USEC)
80+
#define VS1838B_NEC_LEAD_PULSE_OFF_MAX_TICK \
81+
VS1838B_MAX_TICK(NEC_LEAD_PULSE_PERIOD_OFF_USEC, \
82+
VS1838B_NEC_LEAD_PULSE_PERIOD_TOLERANCE_USEC)
83+
84+
#define VS1838B_NEC_BIT_DETECT_MIN_TICK \
85+
VS1838B_MIN_TICK(NEC_BIT_DETECT_PERIOD_USEC, VS1838B_NEC_BIT_DETECT_PERIOD_TOLERANCE_USEC)
86+
#define VS1838B_NEC_BIT_DETECT_MAX_TICK \
87+
VS1838B_MAX_TICK(NEC_BIT_DETECT_PERIOD_USEC, VS1838B_NEC_BIT_DETECT_PERIOD_TOLERANCE_USEC)
88+
89+
#define VS1838B_NEC_BIT_0_TOTAL_MIN_TICK \
90+
VS1838B_MIN_TICK(NEC_BIT_0_TOTAL_PERIOD_USEC, VS1838B_NEC_BIT_0_TOTAL_TOLERANCE_USEC)
91+
#define VS1838B_NEC_BIT_0_TOTAL_MAX_TICK \
92+
VS1838B_MAX_TICK(NEC_BIT_0_TOTAL_PERIOD_USEC, VS1838B_NEC_BIT_0_TOTAL_TOLERANCE_USEC)
93+
94+
#define VS1838B_NEC_BIT_1_TOTAL_MIN_TICK \
95+
VS1838B_MIN_TICK(NEC_BIT_1_TOTAL_PERIOD_USEC, VS1838B_NEC_BIT_1_TOTAL_TOLERANCE_USEC)
96+
#define VS1838B_NEC_BIT_1_TOTAL_MAX_TICK \
97+
VS1838B_MAX_TICK(NEC_BIT_1_TOTAL_PERIOD_USEC, VS1838B_NEC_BIT_1_TOTAL_TOLERANCE_USEC)
98+
99+
struct vs1838b_data {
100+
struct device const *dev;
101+
struct gpio_callback input_cb;
102+
struct k_work_delayable decode_work;
103+
int64_t edges_ticks[NEC_SINGLE_COMMAND_EDGES_COUNT];
104+
uint8_t edges_count;
105+
struct k_sem decode_sem;
106+
};
107+
108+
struct vs1838b_config {
109+
struct gpio_dt_spec input;
110+
};
111+
112+
static inline bool is_within_range(k_ticks_t const ticks, k_ticks_t const min, k_ticks_t const max)
113+
{
114+
return (ticks <= max) && (ticks >= min);
115+
}
116+
117+
static bool read_byte_from(int64_t *const edges_ticks, uint8_t const offset, uint8_t *byte)
118+
{
119+
/* Make sure we add bits from 0 */
120+
uint8_t temp_byte = 0;
121+
k_ticks_t ticks_on;
122+
k_ticks_t ticks_total;
123+
124+
/* Bytes are transmitted LSB first */
125+
for (uint8_t i = 0; i < BITS_PER_BYTE; ++i) {
126+
/*
127+
* To detect bits and their values we analyze:
128+
* - the initial pulse width
129+
* - the total period
130+
*/
131+
ticks_on = edges_ticks[(2 * i) + offset + 1] - edges_ticks[(2 * i) + offset];
132+
ticks_total = edges_ticks[(2 * i) + offset + 2] - edges_ticks[(2 * i) + offset];
133+
134+
LOG_DBG("ticks_on %lld", ticks_on);
135+
LOG_DBG("ticks_total %lld", ticks_total);
136+
if (is_within_range(ticks_on, VS1838B_NEC_BIT_DETECT_MIN_TICK,
137+
VS1838B_NEC_BIT_DETECT_MAX_TICK)) {
138+
if (is_within_range(ticks_total, VS1838B_NEC_BIT_0_TOTAL_MIN_TICK,
139+
VS1838B_NEC_BIT_0_TOTAL_MAX_TICK)) {
140+
/* 0 detected */
141+
} else if (is_within_range(ticks_total, VS1838B_NEC_BIT_1_TOTAL_MIN_TICK,
142+
VS1838B_NEC_BIT_1_TOTAL_MAX_TICK)) {
143+
/* 1 detected */
144+
temp_byte += BIT(i);
145+
} else {
146+
LOG_WRN("Failed to identify detected bit at position %u", i);
147+
return false;
148+
}
149+
} else {
150+
LOG_WRN("Failed to detect a valid bit at position %u", i);
151+
return false;
152+
}
153+
}
154+
155+
*byte = temp_byte;
156+
return true;
157+
}
158+
159+
static bool detect_leading_burst(int64_t *const edges_ticks)
160+
{
161+
/* Detect leading pulse using the first 3 edges */
162+
int64_t lead_ticks_on = edges_ticks[NEC_LEAD_PULSE_EDGE_OFFSET + 1] -
163+
edges_ticks[NEC_LEAD_PULSE_EDGE_OFFSET];
164+
int64_t lead_ticks_off = edges_ticks[NEC_LEAD_PULSE_EDGE_OFFSET + 2] -
165+
edges_ticks[NEC_LEAD_PULSE_EDGE_OFFSET + 1];
166+
167+
/* Manage the corner case of an overflow */
168+
if ((lead_ticks_on < 0) || (lead_ticks_off < 0)) {
169+
LOG_ERR("Ticks overflow: %lld - %lld - %lld",
170+
edges_ticks[NEC_LEAD_PULSE_EDGE_OFFSET],
171+
edges_ticks[NEC_LEAD_PULSE_EDGE_OFFSET + 1],
172+
edges_ticks[NEC_LEAD_PULSE_EDGE_OFFSET + 2]);
173+
return false;
174+
}
175+
176+
LOG_DBG("Read %lld ticks on and %lld ticks off", lead_ticks_on, lead_ticks_off);
177+
178+
return is_within_range(lead_ticks_on, VS1838B_NEC_LEAD_PULSE_ON_MIN_TICK,
179+
VS1838B_NEC_LEAD_PULSE_ON_MAX_TICK) &&
180+
is_within_range(lead_ticks_off, VS1838B_NEC_LEAD_PULSE_OFF_MIN_TICK,
181+
VS1838B_NEC_LEAD_PULSE_OFF_MAX_TICK);
182+
}
183+
184+
static bool read_redundant_byte(int64_t *const edges_ticks, uint8_t *const byte,
185+
uint32_t const offset)
186+
{
187+
uint8_t temp_byte;
188+
uint8_t reverse_byte;
189+
190+
if (read_byte_from(edges_ticks, offset, &temp_byte) &&
191+
read_byte_from(edges_ticks, offset + (2 * BITS_PER_BYTE), &reverse_byte)) {
192+
if (temp_byte == (uint8_t)(~reverse_byte)) {
193+
*byte = temp_byte;
194+
} else {
195+
LOG_ERR("Error while decoding byte");
196+
return false;
197+
}
198+
} else {
199+
LOG_ERR("Error while reading bytes");
200+
return false;
201+
}
202+
203+
return true;
204+
}
205+
206+
static bool read_address_byte(int64_t *const edges_ticks, uint8_t *const address)
207+
{
208+
return read_redundant_byte(edges_ticks, address, NEC_ADDRESS_BYTE_EDGE_OFFSET);
209+
}
210+
211+
static bool read_command_byte(int64_t *const edges_ticks, uint8_t *const command)
212+
{
213+
return read_redundant_byte(edges_ticks, command, NEC_COMMAND_BYTE_EDGE_OFFSET);
214+
}
215+
216+
static bool detect_last_burst(int64_t *const edges_ticks)
217+
{
218+
/* Detect leading pulse using the last 3 edges */
219+
int64_t burst_length = edges_ticks[NEC_SINGLE_COMMAND_EDGES_COUNT - 1] -
220+
edges_ticks[NEC_SINGLE_COMMAND_EDGES_COUNT - 2];
221+
222+
/* Manage the corner case of an overflow */
223+
if (burst_length < 0) {
224+
LOG_ERR("Ticks overflow: %lld - %lld",
225+
edges_ticks[NEC_SINGLE_COMMAND_EDGES_COUNT - 1],
226+
edges_ticks[NEC_SINGLE_COMMAND_EDGES_COUNT - 2]);
227+
return false;
228+
}
229+
230+
LOG_DBG("Read %lld ticks in the last burst", burst_length);
231+
232+
return is_within_range(burst_length, VS1838B_NEC_BIT_DETECT_MIN_TICK,
233+
VS1838B_NEC_BIT_DETECT_MAX_TICK);
234+
}
235+
236+
static bool get_address_and_command(int64_t *const edges_ticks, uint8_t *const address,
237+
uint8_t *const command)
238+
{
239+
if (!detect_leading_burst(edges_ticks)) {
240+
LOG_DBG("No lead detected");
241+
return false;
242+
}
243+
244+
if (!read_address_byte(edges_ticks, address)) {
245+
LOG_DBG("No address decoded");
246+
return false;
247+
}
248+
249+
if (!read_command_byte(edges_ticks, command)) {
250+
LOG_DBG("No command decoded");
251+
return false;
252+
}
253+
if (!detect_last_burst(edges_ticks)) {
254+
LOG_DBG("No trailing edge detected");
255+
return false;
256+
}
257+
258+
return true;
259+
}
260+
261+
/*
262+
* Management of the decoding
263+
*/
264+
static void vs1838b_decode_work_handler(struct k_work *item)
265+
{
266+
struct k_work_delayable *dwork = k_work_delayable_from_work(item);
267+
struct vs1838b_data *data = CONTAINER_OF(dwork, struct vs1838b_data, decode_work);
268+
269+
if (k_sem_take(&data->decode_sem, K_FOREVER) == 0) {
270+
uint8_t address_byte;
271+
uint8_t command_byte;
272+
273+
if (get_address_and_command(data->edges_ticks, &address_byte, &command_byte)) {
274+
LOG_DBG("Address: [0x%X] | Command: [0x%X]", address_byte, command_byte);
275+
if (input_report(data->dev, INPUT_EV_DEVICE, INPUT_MSC_SCAN,
276+
(address_byte << 8) | command_byte, true, K_FOREVER) < 0) {
277+
LOG_ERR("Message failed to be enqueued");
278+
}
279+
}
280+
}
281+
282+
/* Reset the record */
283+
data->edges_count = 0;
284+
k_sem_give(&data->decode_sem);
285+
}
286+
287+
/*
288+
* Internal callback
289+
*/
290+
static void vs1838b_input_callback(struct device const *dev, struct gpio_callback *cb,
291+
uint32_t pins)
292+
{
293+
/*
294+
* We want to:
295+
* - register the timestamps of interrupts
296+
* - try and decode the received bits when we reach the appropriate threshold
297+
*/
298+
int64_t const tick = k_uptime_ticks();
299+
struct vs1838b_data *data = CONTAINER_OF(cb, struct vs1838b_data, input_cb);
300+
301+
/* If we already schedule a decode, we need to cancel it. */
302+
if (k_work_cancel_delayable(&data->decode_work) != 0) {
303+
LOG_WRN("Decoding not cancelled!");
304+
}
305+
306+
if (k_sem_take(&data->decode_sem, K_NO_WAIT) != 0) {
307+
/* Decoding might be pending */
308+
return;
309+
}
310+
311+
/* If more interrupts are received, they're likely to be repeat codes
312+
* and we choose to ignore them.
313+
*/
314+
if (data->edges_count < NEC_SINGLE_COMMAND_EDGES_COUNT) {
315+
data->edges_ticks[data->edges_count++] = tick;
316+
}
317+
318+
if (data->edges_count == NEC_SINGLE_COMMAND_EDGES_COUNT) {
319+
/* There's a candidate!
320+
* If nothing gets in during the grace period
321+
* it *should* be an entire command.
322+
*/
323+
k_work_schedule(&data->decode_work, K_MSEC(NEC_TIMEOUT_REPEAT_CODE_MSEC));
324+
}
325+
k_sem_give(&data->decode_sem);
326+
}
327+
328+
static int vs1838b_init(struct device const *dev)
329+
{
330+
struct vs1838b_config const *config = dev->config;
331+
struct gpio_dt_spec const *data_input = &config->input;
332+
struct vs1838b_data *data = dev->data;
333+
334+
data->dev = dev;
335+
336+
if (!gpio_is_ready_dt(data_input)) {
337+
LOG_ERR("GPIO input pin is not ready");
338+
return -ENODEV;
339+
}
340+
341+
/*
342+
* Setup the input as an interrupt source
343+
* and register an associated callback.
344+
*/
345+
gpio_pin_configure_dt(data_input, GPIO_INPUT);
346+
gpio_pin_interrupt_configure_dt(data_input, GPIO_INT_EDGE_BOTH);
347+
gpio_init_callback(&data->input_cb, vs1838b_input_callback, BIT(data_input->pin));
348+
gpio_add_callback_dt(data_input, &data->input_cb);
349+
350+
k_sem_init(&data->decode_sem, 1, 1);
351+
k_work_init_delayable(&data->decode_work, vs1838b_decode_work_handler);
352+
353+
return 0;
354+
}
355+
356+
#define VS1838B_DEFINE(inst) \
357+
static struct vs1838b_data vs1838b_data_##inst; \
358+
\
359+
static struct vs1838b_config const vs1838b_config_##inst = { \
360+
.input = GPIO_DT_SPEC_INST_GET(inst, data_gpios), \
361+
}; \
362+
\
363+
DEVICE_DT_INST_DEFINE(inst, vs1838b_init, NULL, &vs1838b_data_##inst, \
364+
&vs1838b_config_##inst, POST_KERNEL, CONFIG_INPUT_INIT_PRIORITY, \
365+
NULL);
366+
367+
DT_INST_FOREACH_STATUS_OKAY(VS1838B_DEFINE)

0 commit comments

Comments
 (0)