Skip to content

Commit 151c21a

Browse files
tzarcTonehavenE
authored andcommitted
Extensible split data sync (qmk#11930)
* Extensible split data sync capability through transactions. - Split common transport has been split up between the transport layer and data layer. - Split "transactions" model used, with convergence between I2C and serial data definitions. - Slave matrix "generation count" is used to determine if the full slave matrix needs to be retrieved. - Encoders get the same "generation count" treatment. - All other blocks of data are synchronised when a change is detected. - All transmissions have a globally-configurable deadline before a transmission is forced (`FORCED_SYNC_THROTTLE_MS`, default 100ms). - Added atomicity for all core-synced data, preventing partial updates - Added retries to AVR i2c_master's i2c_start, to minimise the number of failed transactions when interrupts are disabled on the slave due to atomicity checks. - Some keyboards have had slight modifications made in order to ensure that they still build due to firmware size restrictions. * Fixup LED_MATRIX compile. * Parameterise ERROR_DISCONNECT_COUNT.
1 parent ad9fd86 commit 151c21a

File tree

30 files changed

+1388
-676
lines changed

30 files changed

+1388
-676
lines changed

common_features.mk

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,11 @@ ifeq ($(strip $(SPLIT_KEYBOARD)), yes)
524524

525525
# Determine which (if any) transport files are required
526526
ifneq ($(strip $(SPLIT_TRANSPORT)), custom)
527-
QUANTUM_LIB_SRC += $(QUANTUM_DIR)/split_common/transport.c
527+
QUANTUM_SRC += $(QUANTUM_DIR)/split_common/transport.c \
528+
$(QUANTUM_DIR)/split_common/transactions.c
529+
530+
OPT_DEFS += -DSPLIT_COMMON_TRANSACTIONS
531+
528532
# Functions added via QUANTUM_LIB_SRC are only included in the final binary if they're called.
529533
# Unused functions are pruned away, which is why we can add multiple drivers here without bloat.
530534
ifeq ($(PLATFORM),AVR)

docs/config_options.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ There are a few different ways to set handedness for split keyboards (listed in
284284
### Other Options
285285

286286
* `#define USE_I2C`
287-
* For using I2C instead of Serial (defaults to serial)
287+
* For using I2C instead of Serial (default is serial; serial transport is supported on ARM -- I2C is AVR-only)
288288

289289
* `#define SOFT_SERIAL_PIN D0`
290290
* When using serial, define this. `D0` or `D1`,`D2`,`D3`,`E6`.
@@ -312,14 +312,36 @@ There are a few different ways to set handedness for split keyboards (listed in
312312
* `#define SPLIT_USB_DETECT`
313313
* Detect (with timeout) USB connection when delegating master/slave
314314
* Default behavior for ARM
315-
* Required for AVR Teensy
315+
* Required for AVR Teensy (without hardware mods)
316316

317317
* `#define SPLIT_USB_TIMEOUT 2000`
318318
* Maximum timeout when detecting master/slave when using `SPLIT_USB_DETECT`
319319

320320
* `#define SPLIT_USB_TIMEOUT_POLL 10`
321321
* Poll frequency when detecting master/slave when using `SPLIT_USB_DETECT`
322322

323+
* `#define FORCED_SYNC_THROTTLE_MS 100`
324+
* Deadline for synchronizing data from master to slave when using the QMK-provided split transport.
325+
326+
* `#define SPLIT_TRANSPORT_MIRROR`
327+
* Mirrors the master-side matrix on the slave when using the QMK-provided split transport.
328+
329+
* `#define SPLIT_LAYER_STATE_ENABLE`
330+
* Ensures the current layer state is available on the slave when using the QMK-provided split transport.
331+
332+
* `#define SPLIT_LED_STATE_ENABLE`
333+
* Ensures the current host indicator state (caps/num/scroll) is available on the slave when using the QMK-provided split transport.
334+
335+
* `#define SPLIT_MODS_ENABLE`
336+
* Ensures the current modifier state (normal, weak, and oneshot) is available on the slave when using the QMK-provided split transport.
337+
338+
* `#define SPLIT_WPM_ENABLE`
339+
* Ensures the current WPM is available on the slave when using the QMK-provided split transport.
340+
341+
* `#define SPLIT_TRANSACTION_IDS_KB .....`
342+
* `#define SPLIT_TRANSACTION_IDS_USER .....`
343+
* Allows for custom data sync with the slave when using the QMK-provided split transport. See [custom data sync between sides](feature_split_keyboard.md#custom-data-sync) for more information.
344+
323345
# The `rules.mk` File
324346

325347
This is a [make](https://www.gnu.org/software/make/manual/make.html) file that is included by the top-level `Makefile`. It is used to set some information about the MCU that we will be compiling for as well as enabling and disabling certain features.

docs/feature_split_keyboard.md

Lines changed: 104 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ QMK Firmware has a generic implementation that is usable by any board, as well a
88

99
For this, we will mostly be talking about the generic implementation used by the Let's Split and other keyboards.
1010

11-
!> ARM is not yet fully supported for Split Keyboards and has many limitations. Progress is being made, but we have not yet reached 100% feature parity.
12-
11+
!> ARM split supports most QMK subsystems when using the 'serial' and 'serial_usart' drivers. I2C slave is currently unsupported.
1312

1413
## Compatibility Overview
1514

@@ -168,7 +167,7 @@ Because not every split keyboard is identical, there are a number of additional
168167
#define USE_I2C
169168
```
170169

171-
This enables I<sup>2</sup>C support for split keyboards. This isn't strictly for communication, but can be used for OLED or other I<sup>2</sup>C-based devices.
170+
This configures the use of I<sup>2</sup>C support for split keyboard transport (AVR only).
172171

173172
```c
174173
#define SOFT_SERIAL_PIN D0
@@ -192,20 +191,115 @@ If you're having issues with serial communication, you can change this value, as
192191
* **`5`**: about 20kbps
193192
194193
```c
195-
#define SPLIT_MODS_ENABLE
194+
#define FORCED_SYNC_THROTTLE_MS 100
196195
```
197196
198-
This enables transmitting modifier state (normal, weak and oneshot) to the non
199-
primary side of the split keyboard. This adds a few bytes of data to the split
200-
communication protocol and may impact the matrix scan speed when enabled.
201-
The purpose of this feature is to support cosmetic use of modifer state (e.g.
202-
displaying status on an OLED screen).
197+
This sets the maximum number of milliseconds before forcing a synchronization of data from master to slave. Under normal circumstances this sync occurs whenever the data _changes_, for safety a data transfer occurs after this number of milliseconds if no change has been detected since the last sync.
203198
204199
```c
205200
#define SPLIT_TRANSPORT_MIRROR
206201
```
207202
208-
This mirrors the master side matrix to the slave side for features that react or require knowledge of master side key presses on the slave side. This adds a few bytes of data to the split communication protocol and may impact the matrix scan speed when enabled. The purpose of this feature is to support cosmetic use of key events (e.g. RGB reacting to Keypresses).
203+
This mirrors the master side matrix to the slave side for features that react or require knowledge of master side key presses on the slave side. The purpose of this feature is to support cosmetic use of key events (e.g. RGB reacting to keypresses). This adds overhead to the split communication protocol and may negatively impact the matrix scan speed when enabled.
204+
205+
```c
206+
#define SPLIT_LAYER_STATE_ENABLE
207+
```
208+
209+
This enables syncing of the layer state between both halves of the split keyboard. The main purpose of this feature is to enable support for use of things like OLED display of the currently active layer. This adds overhead to the split communication protocol and may negatively impact the matrix scan speed when enabled.
210+
211+
```c
212+
#define SPLIT_LED_STATE_ENABLE
213+
```
214+
215+
This enables syncing of the Host LED status (caps lock, num lock, etc) between both halves of the split keyboard. The main purpose of this feature is to enable support for use of things like OLED display of the Host LED status. This adds overhead to the split communication protocol and may negatively impact the matrix scan speed when enabled.
216+
217+
```c
218+
#define SPLIT_MODS_ENABLE
219+
```
220+
221+
This enables transmitting modifier state (normal, weak and oneshot) to the non primary side of the split keyboard. The purpose of this feature is to support cosmetic use of modifer state (e.g. displaying status on an OLED screen). This adds overhead to the split communication protocol and may negatively impact the matrix scan speed when enabled.
222+
223+
```c
224+
#define SPLIT_WPM_ENABLE
225+
```
226+
227+
This enables transmitting the current WPM to the slave side of the split keyboard. The purpose of this feature is to support cosmetic use of WPM (e.g. displaying the current value on an OLED screen). This adds overhead to the split communication protocol and may negatively impact the matrix scan speed when enabled.
228+
229+
### Custom data sync between sides :id=custom-data-sync
230+
231+
QMK's split transport allows for arbitrary data transactions at both the keyboard and user levels. This is modelled on a remote procedure call, with the master invoking a function on the slave side, with the ability to send data from master to slave, process it slave side, and send data back from slave to master.
232+
233+
To leverage this, a keyboard or user/keymap can define a comma-separated list of _transaction IDs_:
234+
235+
```c
236+
// for keyboard-level data sync:
237+
#define SPLIT_TRANSACTION_IDS_KB KEYBOARD_SYNC_A, KEYBOARD_SYNC_B
238+
// or, for user:
239+
#define SPLIT_TRANSACTION_IDS_USER USER_SYNC_A, USER_SYNC_B, USER_SYNC_C
240+
```
241+
242+
These _transaction IDs_ then need a slave-side handler function to be registered with the split transport, for example:
243+
244+
```c
245+
typedef struct _master_to_slave_t {
246+
int m2s_data;
247+
} master_to_slave_t;
248+
249+
typedef struct _slave_to_master_t {
250+
int s2m_data;
251+
} slave_to_master_t;
252+
253+
void user_sync_a_slave_handler(uint8_t in_buflen, const void* in_data, uint8_t out_buflen, void* out_data) {
254+
const master_to_slave_t *m2s = (const master_to_slave_t*)in_data;
255+
slave_to_master_t *s2m = (slave_to_master_t*)out_data;
256+
s2m->s2m_data = m2s->m2s_data + 5; // whatever comes in, add 5 so it can be sent back
257+
}
258+
259+
void keyboard_post_init_user(void) {
260+
transaction_register_rpc(USER_SYNC_A, user_sync_a_slave_handler);
261+
}
262+
```
263+
264+
The master side can then invoke the slave-side handler - for normal keyboard functionality to be minimally affected, any keyboard- or user-level code attempting to sync data should be throttled:
265+
266+
```c
267+
void housekeeping_task_user(void) {
268+
if (is_keyboard_master()) {
269+
// Interact with slave every 500ms
270+
static uint32_t last_sync = 0;
271+
if (timer_elapsed32(last_sync) > 500) {
272+
master_to_slave_t m2s = {6};
273+
slave_to_master_t s2m = {0};
274+
if(transaction_rpc_exec(USER_SYNC_A, sizeof(m2s), &m2s, sizeof(s2m), &s2m)) {
275+
last_sync = timer_read32();
276+
dprintf("Slave value: %d\n", s2m.s2m_data); // this will now be 11, as the slave adds 5
277+
} else {
278+
dprint("Slave sync failed!\n");
279+
}
280+
}
281+
}
282+
}
283+
```
284+
285+
!> It is recommended that any data sync between halves happens during the master side's _housekeeping task_. This ensures timely retries should failures occur.
286+
287+
If only one-way data transfer is needed, helper methods are provided:
288+
289+
```c
290+
bool transaction_rpc_exec(int8_t transaction_id, uint8_t initiator2target_buffer_size, const void *initiator2target_buffer, uint8_t target2initiator_buffer_size, void *target2initiator_buffer);
291+
bool transaction_rpc_send(int8_t transaction_id, uint8_t initiator2target_buffer_size, const void *initiator2target_buffer);
292+
bool transaction_rpc_recv(int8_t transaction_id, uint8_t target2initiator_buffer_size, void *target2initiator_buffer);
293+
```
294+
295+
By default, the inbound and outbound data is limited to a maximum of 32 bytes each. The sizes can be altered if required:
296+
297+
```c
298+
// Master to slave:
299+
#define RPC_M2S_BUFFER_SIZE 48
300+
// Slave to master:
301+
#define RPC_S2M_BUFFER_SIZE 48
302+
```
209303
210304
### Hardware Configuration Options
211305

drivers/avr/i2c_master.c

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,14 @@
2828
# define F_SCL 400000UL // SCL frequency
2929
#endif
3030

31+
#ifndef I2C_START_RETRY_COUNT
32+
# define I2C_START_RETRY_COUNT 20
33+
#endif // I2C_START_RETRY_COUNT
34+
3135
#define TWBR_val (((F_CPU / F_SCL) - 16) / 2)
3236

37+
#define MAX(X, Y) ((X) > (Y) ? (X) : (Y))
38+
3339
void i2c_init(void) {
3440
TWSR = 0; /* no prescaler */
3541
TWBR = (uint8_t)TWBR_val;
@@ -47,7 +53,7 @@ void i2c_init(void) {
4753
#endif
4854
}
4955

50-
i2c_status_t i2c_start(uint8_t address, uint16_t timeout) {
56+
static i2c_status_t i2c_start_impl(uint8_t address, uint16_t timeout) {
5157
// reset TWI control register
5258
TWCR = 0;
5359
// transmit START condition
@@ -86,6 +92,17 @@ i2c_status_t i2c_start(uint8_t address, uint16_t timeout) {
8692
return I2C_STATUS_SUCCESS;
8793
}
8894

95+
i2c_status_t i2c_start(uint8_t address, uint16_t timeout) {
96+
// Retry i2c_start_impl a bunch times in case the remote side has interrupts disabled.
97+
uint16_t timeout_timer = timer_read();
98+
uint16_t time_slice = MAX(1, (timeout == (I2C_TIMEOUT_INFINITE)) ? 5 : (timeout / (I2C_START_RETRY_COUNT))); // if it's infinite, wait 1ms between attempts, otherwise split up the entire timeout into the number of retries
99+
i2c_status_t status;
100+
do {
101+
status = i2c_start_impl(address, time_slice);
102+
} while ((status < 0) && ((timeout == I2C_TIMEOUT_INFINITE) || (timer_elapsed(timeout_timer) < timeout)));
103+
return status;
104+
}
105+
89106
i2c_status_t i2c_write(uint8_t data, uint16_t timeout) {
90107
// load data into data register
91108
TWDR = data;

drivers/avr/i2c_slave.c

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,20 @@
1717
* GitHub repository: https://github.com/g4lvanix/I2C-slave-lib
1818
*/
1919

20+
#include <stddef.h>
2021
#include <avr/io.h>
2122
#include <util/twi.h>
2223
#include <avr/interrupt.h>
2324
#include <stdbool.h>
2425

2526
#include "i2c_slave.h"
2627

28+
#if defined(USE_I2C) && defined(SPLIT_COMMON_TRANSACTIONS)
29+
# include "transactions.h"
30+
31+
static volatile bool is_callback_executor = false;
32+
#endif // defined(USE_I2C) && defined(SPLIT_COMMON_TRANSACTIONS)
33+
2734
volatile uint8_t i2c_slave_reg[I2C_SLAVE_REG_COUNT];
2835

2936
static volatile uint8_t buffer_address;
@@ -48,22 +55,40 @@ ISR(TWI_vect) {
4855
case TW_SR_SLA_ACK:
4956
// The device is now a slave receiver
5057
slave_has_register_set = false;
58+
#if defined(USE_I2C) && defined(SPLIT_COMMON_TRANSACTIONS)
59+
is_callback_executor = false;
60+
#endif // defined(USE_I2C) && defined(SPLIT_COMMON_TRANSACTIONS)
5161
break;
5262

5363
case TW_SR_DATA_ACK:
5464
// This device is a slave receiver and has received data
55-
// First byte is the location then the bytes will be writen in buffer with auto-incriment
65+
// First byte is the location then the bytes will be writen in buffer with auto-increment
5666
if (!slave_has_register_set) {
5767
buffer_address = TWDR;
5868

5969
if (buffer_address >= I2C_SLAVE_REG_COUNT) { // address out of bounds dont ack
6070
ack = 0;
6171
buffer_address = 0;
6272
}
63-
slave_has_register_set = true; // address has been receaved now fill in buffer
73+
slave_has_register_set = true; // address has been received now fill in buffer
74+
75+
#if defined(USE_I2C) && defined(SPLIT_COMMON_TRANSACTIONS)
76+
// Work out if we're attempting to execute a callback
77+
is_callback_executor = buffer_address == split_transaction_table[I2C_EXECUTE_CALLBACK].initiator2target_offset;
78+
#endif // defined(USE_I2C) && defined(SPLIT_COMMON_TRANSACTIONS)
6479
} else {
6580
i2c_slave_reg[buffer_address] = TWDR;
6681
buffer_address++;
82+
83+
#if defined(USE_I2C) && defined(SPLIT_COMMON_TRANSACTIONS)
84+
// If we're intending to execute a transaction callback, do so, as we've just received the transaction ID
85+
if (is_callback_executor) {
86+
split_transaction_desc_t *trans = &split_transaction_table[split_shmem->transaction_id];
87+
if (trans->slave_callback) {
88+
trans->slave_callback(trans->initiator2target_buffer_size, split_trans_initiator2target_buffer(trans), trans->target2initiator_buffer_size, split_trans_target2initiator_buffer(trans));
89+
}
90+
}
91+
#endif // defined(USE_I2C) && defined(SPLIT_COMMON_TRANSACTIONS)
6792
}
6893
break;
6994

drivers/avr/i2c_slave.h

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,18 @@
2222

2323
#pragma once
2424

25-
#define I2C_SLAVE_REG_COUNT 30
25+
#ifndef I2C_SLAVE_REG_COUNT
26+
27+
# if defined(USE_I2C) && defined(SPLIT_COMMON_TRANSACTIONS)
28+
# include "transport.h"
29+
# define I2C_SLAVE_REG_COUNT sizeof(split_shared_memory_t)
30+
# else // defined(USE_I2C) && defined(SPLIT_COMMON_TRANSACTIONS)
31+
# define I2C_SLAVE_REG_COUNT 30
32+
# endif // defined(USE_I2C) && defined(SPLIT_COMMON_TRANSACTIONS)
33+
34+
#endif // I2C_SLAVE_REG_COUNT
35+
36+
_Static_assert(I2C_SLAVE_REG_COUNT < 256, "I2C target registers must be single byte");
2637

2738
extern volatile uint8_t i2c_slave_reg[I2C_SLAVE_REG_COUNT];
2839

0 commit comments

Comments
 (0)