Skip to content

Commit 116bb5c

Browse files
authored
Merge pull request #42 from loopj/multi-target
Add native support for multiple chained targets
2 parents 716efb5 + deb2d22 commit 116bb5c

10 files changed

Lines changed: 430 additions & 27 deletions

File tree

‎include/joybus/bus.h‎

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,11 @@ struct joybus {
115115
/** The frequency of the bus, in Hz. */
116116
uint32_t freq;
117117

118-
/** The target device attached to this Joybus instance, if any. */
119-
struct joybus_target *target;
118+
/** The targets attached to this Joybus instance, in attachment order. */
119+
struct joybus_target *targets;
120+
121+
/** The target that claimed the command being received, if any. */
122+
struct joybus_target *active_target;
120123

121124
uint8_t command_buffer[JOYBUS_BLOCK_SIZE];
122125
uint8_t response_buffer[JOYBUS_BLOCK_SIZE];
@@ -192,17 +195,16 @@ int joybus_transfer_sync(struct joybus *bus, const uint8_t *write_buf, uint8_t w
192195
/**
193196
* Attach a target to handle commands received in target mode.
194197
*
198+
* A bus accepts more than one target. Each command is offered to the attached
199+
* targets in attachment order, so a target attached earlier takes priority
200+
* over those attached after it. Attaching a target that is already attached
201+
* has no effect.
202+
*
195203
* @param bus the Joybus instance to use
196204
* @param target the target to attach
197205
* @return 0 on success, a negative joybus_error on failure
198206
*/
199-
static inline int joybus_attach_target(struct joybus *bus, struct joybus_target *target)
200-
{
201-
bus->target = target;
202-
target->attached = true;
203-
204-
return 0;
205-
}
207+
int joybus_attach_target(struct joybus *bus, struct joybus_target *target);
206208

207209
/**
208210
* Detach a target from the bus.
@@ -211,12 +213,44 @@ static inline int joybus_attach_target(struct joybus *bus, struct joybus_target
211213
* @param target the target to detach
212214
* @return 0 on success, a negative joybus_error on failure
213215
*/
214-
static inline int joybus_detach_target(struct joybus *bus, struct joybus_target *target)
215-
{
216-
bus->target = NULL;
217-
target->attached = false;
216+
int joybus_detach_target(struct joybus *bus, struct joybus_target *target);
218217

219-
return 0;
218+
/**
219+
* Offer a received command byte to the targets attached to the bus.
220+
*
221+
* Called by backends in target mode as each command byte arrives. The first
222+
* byte of a command is offered to each attached target in attachment order
223+
* until one claims it by returning anything other than -JOYBUS_ERR_NOT_SUPPORTED.
224+
* Every later byte of the command goes to that target alone.
225+
*
226+
* @param bus the Joybus instance that received the byte
227+
* @param command the command buffer
228+
* @param byte_idx the index of the byte that was just received
229+
* @param send_response a callback function to send the response
230+
* @param user_data user data to pass to the response callback
231+
* @return positive number of bytes still expected, 0 if no more bytes expected, a negative joybus_error on failure.
232+
* -JOYBUS_ERR_NOT_SUPPORTED if no target claimed the command
233+
*/
234+
static inline int joybus_byte_received(struct joybus *bus, const uint8_t *command, uint8_t byte_idx,
235+
joybus_target_response_cb send_response, void *user_data)
236+
{
237+
// Later bytes go straight to the target that claimed the command
238+
if (byte_idx > 1)
239+
return joybus_target_byte_received(bus->active_target, command, byte_idx, send_response, user_data);
240+
241+
// Offer the first byte to each target in attachment order until one claims the command
242+
for (struct joybus_target *target = bus->targets; target; target = target->next) {
243+
int rc = joybus_target_byte_received(target, command, byte_idx, send_response, user_data);
244+
if (rc == -JOYBUS_ERR_NOT_SUPPORTED)
245+
continue;
246+
247+
bus->active_target = target;
248+
return rc;
249+
}
250+
251+
// No target claimed the command
252+
bus->active_target = NULL;
253+
return -JOYBUS_ERR_NOT_SUPPORTED;
220254
}
221255

222256
// Context for a blocking Joybus operation

‎include/joybus/target.h‎

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,22 @@
2626
* at a ::joybus_target_api table, and attach it to a bus with
2727
* joybus_attach_target().
2828
*
29+
* A bus accepts more than one target. Each command is offered to the attached
30+
* targets in attachment order, and the first one that accepts it handles that
31+
* command to the end. This places the following contract on a target:
32+
*
33+
* - A target decides on the first byte of a command. Returning
34+
* -JOYBUS_ERR_NOT_SUPPORTED from the handler for the first byte declines the
35+
* command, and the byte is then offered to the next target. Any other return
36+
* claims the command, and the target receives every later byte of it. A
37+
* target cannot claim a command later.
38+
* - Declining has no side effects. A declined first byte must store nothing
39+
* and send nothing, since another target may go on to handle the command.
40+
* - Once a command is claimed, -JOYBUS_ERR_NOT_SUPPORTED from a later byte is
41+
* an error in a command the target owns, and the bus returns to idle.
42+
* - On the first byte, "not my opcode" and "my opcode, refused" are the same
43+
* return, so a target cannot distinguish them.
44+
*
2945
* @{
3046
*/
3147

@@ -63,7 +79,8 @@ struct joybus_target_api {
6379
* @param byte_idx the index of the byte that was just received
6480
* @param send_response a callback function to send the response
6581
* @param user_data user data to pass to the response callback
66-
* @return positive number of bytes still expected, 0 if no more bytes expected, a negative joybus_error on failure
82+
* @return positive number of bytes still expected, 0 if no more bytes expected, a negative joybus_error on failure.
83+
* -JOYBUS_ERR_NOT_SUPPORTED on the first byte declines the command, see the target contract above
6784
*/
6885
int (*byte_received)(struct joybus_target *target, const uint8_t *command, uint8_t byte_idx,
6986
joybus_target_response_cb send_response, void *user_data);
@@ -78,10 +95,16 @@ struct joybus_target {
7895

7996
/// Whether the target is currently attached to a bus
8097
bool attached;
98+
99+
/// The next target attached to the same bus, in attachment order
100+
struct joybus_target *next;
81101
};
82102

83103
/**
84-
* Handle a received command byte for a Joybus target.
104+
* Handle a received command byte for a single Joybus target.
105+
*
106+
* Backends do not call this directly. They call joybus_byte_received(), which
107+
* offers the command to each target attached to the bus.
85108
*
86109
* @param target the target to handle the command
87110
* @param command the command buffer

‎libjoybus.slcc‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ include:
1818
- path: include
1919

2020
source:
21+
- path: src/bus.c
2122
- path: src/checksum.c
2223
- path: src/backend/gecko_sdk/joybus.c
2324
- path: src/host/common.c

‎src/backend/README.md‎

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ int joybus_mybackend_init(struct joybus_mybackend *mybackend_bus, struct joybus_
2121
struct joybus *bus = JOYBUS(mybackend_bus);
2222
bus->api = &mybackend_api;
2323
bus->freq = config.freq;
24-
bus->target = NULL;
24+
bus->targets = NULL;
25+
bus->active_target = NULL;
2526

2627
// Rest of initialization code...
2728

@@ -59,14 +60,29 @@ The turnaround times between transmitting and receiving data are also critical.
5960
6061
- When enabled, target mode should await bus idle before receiving a command. The bus is idle once the line has been continuously high for 100us (`JOYBUS_BUS_IDLE_US`)
6162
- A falling edge on the bus indicates the start of a command
62-
- Received bytes are streamed to the target: `joybus_target_byte_received` should be called as soon as possible after each complete byte is received
63+
- Received bytes are streamed to the targets: `joybus_byte_received` should be called as soon as possible after each complete byte is received. It offers the first byte of a command to each attached target in attachment order, and routes every later byte to the target that claimed the command
6364
- After the first byte is received, if more bytes are expected, they should be received contiguously, otherwise wait for the next command (await bus idle) - timeout mechanism can be determined by the backend (per bit, per byte, etc)
64-
- If a byte is received and there is no registered target, ignore it and wait for the next command (await bus idle)
65-
- If the registered target does not recognize the command's opcode, ignore it and wait for the next command (await bus idle)
66-
- The registered target can call the `joybus_target_response_cb` at any time to signal to the backend that response bytes are available
65+
- If `joybus_byte_received` returns an error, including when no target is attached or no attached target recognizes the command's opcode, ignore the command and wait for the next command (await bus idle)
66+
- The target handling a command can call the `joybus_target_response_cb` at any time to signal to the backend that response bytes are available
6767
- Since the `joybus_target_response_cb` can be called before the last byte, we can use this as an opportunity to begin pre-encoding the response while waiting for the remaining bytes to be clocked in
6868
- Once the final byte of a command is received, we must begin clocking out the response IMMEDIATELY (OEM devices turn around in 3.1 - 4.4us, or 6.6 - 7.0us for a GBA cable, see below for measured OEM timings)
6969
70+
## Reducing Turnaround
71+
72+
The hard deadline in both modes is the turnaround between the last bit of a command and the first bit of the reply. These are the techniques the existing backends use to meet it.
73+
74+
- Stream command bytes to the target as they arrive. `joybus_byte_received` is called once per byte rather than once per command, so a target can recognize a command from its opcode and call `joybus_target_response_cb` before the final byte has been clocked in. Every technique below depends on the backend holding the response before the command ends.
75+
- Pre-encode the response into the wire format while the remaining bits arrive. The gecko backend encodes the first one or two bytes in `prepare_write` and encodes each later byte from the transmit DMA interrupt, so encoding never blocks the wire.
76+
- Arm the transmitter early and gate it, so starting the reply costs a single write. The gecko backend starts its transmit DMA with the peripheral request disabled (`REQDIS_SET`) and releases it with `REQDIS_CLR` once the last byte lands. The rp2xxx backend starts its transmit DMA as soon as the response arrives, which fills the PIO transmit FIFO while the state machine is still in its receive loop, then switches with a single `jmp`.
77+
- Start transmitting before the whole response is encoded. The esp32 backend writes the first symbol of the first byte, starts the transmitter immediately, and fills the remaining symbols behind the hardware as it reads them.
78+
- Arm the receiver before the reply can arrive, never in response to a transmit-complete interrupt. The esp32 backend arms RX before it transmits and captures the command, the stop bit, and the reply as one continuous capture, then decodes the reply from a fixed symbol offset. This removes interrupt latency from the path and works regardless of MCU clock speed.
79+
- Let the hardware perform the turnaround where the peripheral allows it. The rp2xxx host PIO program sends the stop bit, fires its interrupt, and falls straight into its receive loop, so no CPU work sits between the end of the command and being ready to sample the reply.
80+
- Fire the byte-received interrupt early and decode underneath the bits still arriving. The esp32 backend sets the RMT receive limit a symbol or two short of a full byte, folds in each bit as its symbol commits, and busy-waits only for the last one. Interrupt latency is hidden behind the tail of the byte instead of being added after it.
81+
- Do setup work while the bus is idle. The esp32 backend resets its transmit pointer when it enters receive mode, keeping that work off the critical path.
82+
- Keep the hot path out of flash. The esp32 backend marks its encode, decode, and interrupt handlers `IRAM_ATTR`. Running the target handler and checksum from flash measurably slows the reply on parts that fetch instructions from external flash.
83+
84+
Replying faster than an OEM device has no known benefit. The esp32 backend enforces a floor (`TARGET_REPLY_FLOOR_NS`) so it never replies much sooner than the measured OEM turnaround below.
85+
7086
## Checklist
7187
7288
When implementing a new backend, the following requirements are considered the bare minimum:

‎src/backend/esp32/joybus.c‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ static inline IRAM_ATTR void target_byte_received(struct joybus *bus)
368368
data->read_count++;
369369

370370
// Call the target handler to prepare a response if needed
371-
int rc = joybus_target_byte_received(bus->target, data->read_buf, data->read_count, handle_command_response, bus);
371+
int rc = joybus_byte_received(bus, data->read_buf, data->read_count, handle_command_response, bus);
372372
if (rc == 0) {
373373
// No more bytes expected
374374
if (data->write_len > 0) {
@@ -717,7 +717,8 @@ int joybus_esp32_init(struct joybus_esp32 *esp32_bus, struct joybus_esp32_config
717717
// Save the bus API and common configuration
718718
struct joybus *bus = JOYBUS(esp32_bus);
719719
bus->api = &esp32_api;
720-
bus->target = NULL;
720+
bus->targets = NULL;
721+
bus->active_target = NULL;
721722
bus->freq = config.freq;
722723

723724
// Save the ESP32-specific configuration and initialize state

‎src/backend/gecko_sdk/joybus.c‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ static bool ldma_rx_handler(unsigned int chan, unsigned int iteration, void *use
294294
data->rx_current_buffer ^= 1;
295295

296296
// Handle the received byte
297-
int rc = joybus_target_byte_received(bus->target, data->read_buf, iteration, handle_command_response, bus);
297+
int rc = joybus_byte_received(bus, data->read_buf, iteration, handle_command_response, bus);
298298
if (rc == 0) {
299299
// No more bytes expected
300300
// Start the response transfer if there is one
@@ -697,7 +697,8 @@ int joybus_gecko_init(struct joybus_gecko *gecko_bus, struct joybus_gecko_config
697697
struct joybus *bus = JOYBUS(gecko_bus);
698698
bus->api = &gecko_api;
699699
bus->freq = config.freq;
700-
bus->target = NULL;
700+
bus->targets = NULL;
701+
bus->active_target = NULL;
701702

702703
// Save the joybus configuration
703704
struct joybus_gecko_data *data = &gecko_bus->data;

‎src/backend/rp2xxx/joybus.c‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ static inline void target_byte_received(struct joybus *bus)
219219
data->read_count++;
220220

221221
// Call the target handler to prepare a response if needed
222-
int rc = joybus_target_byte_received(bus->target, data->read_buf, data->read_count, handle_command_response, bus);
222+
int rc = joybus_byte_received(bus, data->read_buf, data->read_count, handle_command_response, bus);
223223
if (rc == 0) {
224224
// No more bytes expected, start transmitting the response
225225
pio_sm_exec(data->pio, data->pio_sm,
@@ -402,7 +402,8 @@ int joybus_rp2xxx_init(struct joybus_rp2xxx *rp2xxx_bus, struct joybus_rp2xxx_co
402402
struct joybus *bus = JOYBUS(rp2xxx_bus);
403403
bus->api = &rp2xxx_api;
404404
bus->freq = config.freq;
405-
bus->target = NULL;
405+
bus->targets = NULL;
406+
bus->active_target = NULL;
406407

407408
// Save the joybus configuration
408409
struct joybus_rp2xxx_data *data = &rp2xxx_bus->data;

‎src/bus.c‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,42 @@
11
#include <joybus/bus.h>
22

3+
int joybus_attach_target(struct joybus *bus, struct joybus_target *target)
4+
{
5+
if (target->attached)
6+
return 0;
7+
8+
// Append the target after those already attached, so earlier targets keep priority
9+
struct joybus_target **tail = &bus->targets;
10+
while (*tail)
11+
tail = &(*tail)->next;
12+
13+
target->next = NULL;
14+
*tail = target;
15+
target->attached = true;
16+
17+
return 0;
18+
}
19+
20+
int joybus_detach_target(struct joybus *bus, struct joybus_target *target)
21+
{
22+
// Unlink the target from the list of attached targets
23+
for (struct joybus_target **link = &bus->targets; *link; link = &(*link)->next) {
24+
if (*link != target)
25+
continue;
26+
27+
*link = target->next;
28+
target->next = NULL;
29+
target->attached = false;
30+
break;
31+
}
32+
33+
// Stop delivering the command in progress to a target that is no longer attached
34+
if (bus->active_target == target)
35+
bus->active_target = NULL;
36+
37+
return 0;
38+
}
39+
340
void joybus_sync_cb(struct joybus *bus, int status, void *user_data)
441
{
542
struct joybus_sync_ctx *ctx = (struct joybus_sync_ctx *)user_data;

‎test/CMakeLists.txt‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ endfunction()
2323
# Checksum tests
2424
add_libjoybus_test(test_checksum test_checksum.c)
2525

26+
# Bus target attachment and command dispatch tests
27+
add_libjoybus_test(test_bus test_bus.c)
28+
2629
# GameCube controller target tests
2730
add_libjoybus_test(test_gcn_controller target/test_gcn_controller.c)
2831

0 commit comments

Comments
 (0)