|
rcbench
Servo and ESC test bench: panel, IOmcu and shared C
|
The output bank. More...
Functions | |
| const out_driver_def_t * | out_driver (out_driver_t d) |
| The table itself, so callers can ask rather than assume. | |
| uint16_t | outputs_role_rest (out_role_t role) |
The command a channel of role rests at, 0..OUT_SPAN of its own endpoints: 0 for a throttle, OUT_SPAN / 2 for a surface. | |
| void | outputs_init (outputs_t *o, uint32_t now_ms) |
| Set the bank to its start: every channel a surface at rest, the endpoints 1000 to 2000us, and the timeout OUT_DEFAULT_TIMEOUT_MS. | |
| void | outputs_reserve_pins (outputs_t *o, uint64_t mask) |
| Declare the pins this build must never drive, one bit per pin. | |
| bool | outputs_pin_available (const outputs_t *o, uint8_t pin) |
Whether pin may be driven: it exists and is not in the reserved mask. | |
| static bool | overlaps (unsigned a0, unsigned an, unsigned b0, unsigned bn) |
| Does [a0,a0+an) meet [b0,b0+bn)? | |
| bool | outputs_configure (outputs_t *o, uint8_t slot, const out_slot_t *cfg) |
| Configure one slot, or clear it with OUT_DRIVER_NONE. | |
| bool | outputs_set_endpoints (outputs_t *o, uint8_t ch, uint16_t min_us, uint16_t max_us) |
| Endpoints, refused rather than clamped. | |
| bool | outputs_set_role (outputs_t *o, uint8_t ch, out_role_t role) |
| Set the role of a channel: throttle or surface. | |
| bool | outputs_set_slew (outputs_t *o, uint8_t ch, uint16_t per_s) |
| Set how fast a channel moves towards its command. | |
| bool | outputs_set (outputs_t *o, uint8_t ch, uint16_t command, uint32_t now_ms) |
| Command a channel. | |
| unsigned | outputs_set_role_channels (outputs_t *o, out_role_t role, uint8_t limit, uint16_t command, uint32_t now_ms) |
Command every channel below limit that carries role, and say how many took it. | |
| bool | outputs_keepalive (outputs_t *o, uint8_t ch, uint32_t now_ms) |
| Say a channel is still being watched without changing what it was asked. | |
| bool | outputs_armed (const outputs_t *o) |
| Whether the bank is armed, for a screen that shows it. | |
| void | outputs_arm (outputs_t *o, bool armed, uint32_t now_ms) |
| Arming is decided by the end holding the wire, not by the asker. | |
| void | outputs_arm_roles (outputs_t *o, uint8_t roles, uint32_t now_ms) |
Arm the channels of roles, OUT_ROLE_BIT()s, and only those. 0 disarms. outputs_arm() is this with OUT_ROLES_ALL. | |
| uint8_t | outputs_armed_roles (const outputs_t *o) |
| The roles the bank is armed for: 0 while disarmed. | |
| bool | outputs_role_armed (const outputs_t *o, out_role_t role) |
Whether the bank is armed for role. | |
| bool | outputs_channel_driving (const outputs_t *o, uint8_t ch) |
Whether ch is driven: the bank is armed for its role. | |
| bool | outputs_slot_driving (const outputs_t *o, uint8_t slot) |
Whether slot is driven: it is configured and the bank is armed for the role of at least one of its channels. | |
| bool | outputs_overdue (const outputs_t *o, uint8_t ch, uint32_t now_ms) |
| Has this channel stopped being commanded? | |
| void | outputs_all_off (outputs_t *o) |
| Everything to rest, immediately. | |
| void | outputs_step (outputs_t *o, uint32_t now_ms) |
Advance slew to now_ms, and put a channel nobody has commanded for timeout_ms at its role's rest. | |
| uint16_t | outputs_actual (const outputs_t *o, uint8_t ch) |
| The position a channel has reached. | |
| uint16_t | outputs_command (const outputs_t *o, uint8_t ch) |
What ch is being asked for, as against outputs_actual()'s where the slew and the staleness timeout have got it to. | |
| uint16_t | outputs_pulse_us (const outputs_t *o, uint8_t ch) |
What a pulse driver should emit for ch, in microseconds. | |
| bool | outputs_driving (const outputs_t *o) |
| Whether the bank is armed for any role. | |
Variables | |
| static const out_driver_def_t | k_drivers [OUT_DRIVER_COUNT] |
| The driver table, indexed by out_driver_t. | |
| const out_driver_def_t * out_driver | ( | out_driver_t | d | ) |
The table itself, so callers can ask rather than assume.
| d | The driver to look up. |
d, or NULL when d is not a driver. | uint16_t outputs_actual | ( | const outputs_t * | o, |
| uint8_t | ch ) |
The position a channel has reached.
| o | The bank. |
| ch | Channel index, 0 to OUT_MAX_CHANNELS - 1. |
| void outputs_all_off | ( | outputs_t * | o | ) |
Everything to rest, immediately.
The failsafe edge calls this, and it reaches every output because every output is in this bank.
| o | The bank. Nothing is done when NULL. |
| void outputs_arm | ( | outputs_t * | o, |
| bool | armed, | ||
| uint32_t | now_ms ) |
Arming is decided by the end holding the wire, not by the asker.
Disarming goes to rest with no ramp.
Re-asserting the current state is not an event and does nothing, so the call is safe every pass from a loop that recomputes whether driving is allowed. Stamping the clock on every call would hold every channel alive and defeat the timeout.
| o | The bank. |
| armed | true to arm for every role, false to disarm. |
| now_ms | Current time in milliseconds. |
| void outputs_arm_roles | ( | outputs_t * | o, |
| uint8_t | roles, | ||
| uint32_t | now_ms ) |
Arm the channels of roles, OUT_ROLE_BIT()s, and only those. 0 disarms. outputs_arm() is this with OUT_ROLES_ALL.
A channel of a role outside the set is not driven: outputs_step() holds it at its rest and outputs_channel_driving() says no. A bank armed for the throttle leaves a surface bound beside it without a pulse, and the other way round.
Changing the set while armed is applied at once: a channel whose role leaves goes to rest with no ramp, as at a disarm, and a channel whose role joins is stamped fresh, as at an arm. Re-asserting the set in force is not an event. Bits outside OUT_ROLES_ALL are ignored.
| o | The bank. |
| roles | Set of roles to arm, OUT_ROLE_BIT() bits. 0 disarms. |
| now_ms | Current time in milliseconds. |
| bool outputs_armed | ( | const outputs_t * | o | ) |
Whether the bank is armed, for a screen that shows it.
| o | The bank. |
| uint8_t outputs_armed_roles | ( | const outputs_t * | o | ) |
The roles the bank is armed for: 0 while disarmed.
| o | The bank. |
| bool outputs_channel_driving | ( | const outputs_t * | o, |
| uint8_t | ch ) |
Whether ch is driven: the bank is armed for its role.
| o | The bank. |
| ch | Channel index, 0 to OUT_MAX_CHANNELS - 1. |
| uint16_t outputs_command | ( | const outputs_t * | o, |
| uint8_t | ch ) |
What ch is being asked for, as against outputs_actual()'s where the slew and the staleness timeout have got it to.
A channel nobody has commanded is asking for its role's rest.
| o | The bank. |
| ch | Channel index, 0 to OUT_MAX_CHANNELS - 1. |
| bool outputs_configure | ( | outputs_t * | o, |
| uint8_t | slot, | ||
| const out_slot_t * | cfg ) |
Configure one slot, or clear it with OUT_DRIVER_NONE.
Refuses a driver it does not know, a channel count the driver cannot take, a channel range that runs off the end, a rate the driver cannot produce, a pin another slot is already driving, and a channel another slot is already rendering.
OUT_DRIVER_NONE clears the slot and is accepted for any valid slot index.
| o | The bank. |
| slot | Slot index, 0 to OUT_MAX_SLOTS - 1. |
| cfg | The configuration to store. |
o or cfg is NULL, the slot index is out of range, or a check above refuses it. | bool outputs_driving | ( | const outputs_t * | o | ) |
Whether the bank is armed for any role.
The same answer as outputs_armed(), asked by a driver rather than by a screen. Which pins carry edges is outputs_slot_driving() and outputs_channel_driving(): an arm covers the roles it names and no others.
Two gates a driver also stands behind are not in this answer:
The heartbeat. Nothing here can see the safety line. What armed means is settled by the end holding the wire before it calls outputs_arm(): the IOmcu recomputes it every pass from the ARM register, its link failsafe and its heartbeat monitor, so a bank armed there already carries a beat that was judged this pass. "This pass" is the whole of the guarantee: silence is noticed by a poll, so the line can have been dead for up to one pass of that loop before the poll says so, and the monostable is what covers that gap in hardware.
A command arriving. That is per channel, outputs_overdue(), and it decides what a channel renders rather than whether it renders. outputs_step() puts an overdue channel at its rest and leaves the others where they are.
Limitation: an armed bank drives every bound pin of the roles it is armed for, whether or not anything is commanding it. A channel that has had no command for timeout_ms is rendered at its role's rest. A surface's rest is OUT_SPAN/2, which outputs_pulse_us() renders as the midpoint of that channel's own endpoints: 1500us across the default 1000 to 2000us, and 760us across the 660 to 860us of a narrow servo. Endpoints are per channel and are written over the CHAN_CFG page, so a channel carrying the wrong role presents the midpoint of whatever range it was given, for as long as the bank is armed.
The timeout does reach that channel. outputs_overdue() is true for it and outputs_step() acts on it every pass; what the timeout resolves to is the rest the channel is already at, so the edges stay on the pin. A disarm is what takes them away.
Nor is a wrong role out of reach of every command. Channels are addressed two ways. outputs_set_role_channels() addresses them by role and passes a surface by, which is why the throttle does not reach one. outputs_set() and outputs_channels_apply_n() address them by index, and the CHANNELS page is written that way, so a write there commands a channel whatever its role.
Rest is not where a channel sits for the first timeout_ms after an arm either. outputs_arm() stamps last_command_ms on all OUT_MAX_CHANNELS channels, so a command given while disarmed is fresh again and is rendered until it goes overdue. What gets rendered in that time is the slew's answer, not the command:
slew_per_s 0 the first step is the whole distance, so the bank carries the last command it was left with for the whole timeout – 500ms at OUT_DEFAULT_TIMEOUT_MS slew_per_s > 0 the disarm has already put actual at rest, so the bank carries a ramp from rest towards that command. It arrives only if the distance is under slew_per_s * timeout_ms / 1000, half of slew_per_s at the default timeout; past that the timeout returns it to rest with the command never reached. The timeout runs from the arm, not from the arrival. That bound holds however often the caller steps, because outputs_step() carries the part-unit between steps rather than rounding each one up.
A throttle rests at 0, so its re-arm ramp is always upward and is slewed. A surface rests at the middle of its travel and ramps either way.
The bank is what carries it; the pin can lag. A driver with a setup sequence sends that first: firmware/iomcu/src/outputs_hw.c sends DSHOT_CMD_EDT_ENABLE for DSHOT_CMD_REPEATS (10) frames on the edge into driving for a bidirectional DShot slot, which at the IOmcu's 1,000Hz update rate is the first 10ms of the arm. The command reaches that ESC after them.
What an ESC (electronic speed controller) does with the surface rest is a question about the ESC. A receiver output of 1500us is about half throttle. Whether one that has seen no pulses and is then handed 1500us runs there or refuses to arm is not measured on this bench.
| o | The bank. |
o is NULL. | void outputs_init | ( | outputs_t * | o, |
| uint32_t | now_ms ) |
Set the bank to its start: every channel a surface at rest, the endpoints 1000 to 2000us, and the timeout OUT_DEFAULT_TIMEOUT_MS.
Nothing is done when o is NULL. The bank starts disarmed, with every role in its arm set.
| o | The bank, overwritten. |
| now_ms | Current time in milliseconds, stamped on every channel. |
| bool outputs_keepalive | ( | outputs_t * | o, |
| uint8_t | ch, | ||
| uint32_t | now_ms ) |
Say a channel is still being watched without changing what it was asked.
A loop that is running with nothing new to say differs from one that has stopped, and the timeout exists to tell those apart. Re-sending the last command would also overwrite a command that arrived from elsewhere in the same pass.
| o | The bank. |
| ch | Channel index, 0 to OUT_MAX_CHANNELS - 1. |
| now_ms | Current time in milliseconds, stamped as the last command time. |
| bool outputs_overdue | ( | const outputs_t * | o, |
| uint8_t | ch, | ||
| uint32_t | now_ms ) |
Has this channel stopped being commanded?
Per channel rather than per bank, because the pages that write them are written independently: a servo being dragged does not keep a throttle alive, and a throttle nobody has touched must not stop a servo.
| o | The bank. |
| ch | Channel index, 0 to OUT_MAX_CHANNELS - 1. |
| now_ms | Current time in milliseconds. |
| bool outputs_pin_available | ( | const outputs_t * | o, |
| uint8_t | pin ) |
Whether pin may be driven: it exists and is not in the reserved mask.
The reserved mask is the only test here. Slots that already drive a pin are tested by outputs_configure().
| o | The bank. |
| pin | GPIO number. |
pin is at most OUT_MAX_PIN and not in the reserved mask, false otherwise or for NULL. | uint16_t outputs_pulse_us | ( | const outputs_t * | o, |
| uint8_t | ch ) |
What a pulse driver should emit for ch, in microseconds.
The actual value mapped onto the channel's min_us..max_us endpoints, rounded to the nearest microsecond.
| o | The bank. |
| ch | Channel index, 0 to OUT_MAX_CHANNELS - 1. |
| void outputs_reserve_pins | ( | outputs_t * | o, |
| uint64_t | mask ) |
Declare the pins this build must never drive, one bit per pin.
Which pins exist and which are already spoken for is the one thing about an output the bank cannot work out for itself: it is a property of the board and the firmware around it, not of the protocol. The pin arrives from the host over the OUTPUTS page and is therefore whatever an operator typed, so something has to hold the list of pins that already carry the CAN (Controller Area Network) controller, the safety line and the console.
Setting the mask does not tear down a slot that is already using a pin in it. Reserve before configuring; the IOmcu does this once at boot, and the panel leaves the mask empty because it drives no pin of its own.
| o | The bank. |
| mask | Reserved pins, bit n for GPn. Pins above OUT_MAX_PIN are not reachable and need no bit. |
| bool outputs_role_armed | ( | const outputs_t * | o, |
| out_role_t | role ) |
Whether the bank is armed for role.
| o | The bank. |
| role | The role to test. |
| uint16_t outputs_role_rest | ( | out_role_t | role | ) |
The command a channel of role rests at, 0..OUT_SPAN of its own endpoints: 0 for a throttle, OUT_SPAN / 2 for a surface.
What an armed bank renders on a channel nobody commands, and what a screen that shows such a channel's value shows.
| role | The channel's role. |
| bool outputs_set | ( | outputs_t * | o, |
| uint8_t | ch, | ||
| uint16_t | command, | ||
| uint32_t | now_ms ) |
Command a channel.
Clamped, and remembered even while disarmed.
| o | The bank. |
| ch | Channel index, 0 to OUT_MAX_CHANNELS - 1. |
| command | Asked position, 0 to OUT_SPAN; a larger value is clamped to OUT_SPAN. |
| now_ms | Current time in milliseconds, stamped as the last command. |
| bool outputs_set_endpoints | ( | outputs_t * | o, |
| uint8_t | ch, | ||
| uint16_t | min_us, | ||
| uint16_t | max_us ) |
Endpoints, refused rather than clamped.
Inverted pairs are straightened: an inverted pair is a mistake rather than a range, and it would make the clamp unsatisfiable.
| o | The bank. |
| ch | Channel index, 0 to OUT_MAX_CHANNELS - 1. |
| min_us | Pulse width in microseconds for command 0. |
| max_us | Pulse width in microseconds for OUT_SPAN. |
| bool outputs_set_role | ( | outputs_t * | o, |
| uint8_t | ch, | ||
| out_role_t | role ) |
Set the role of a channel: throttle or surface.
A role change moves the channel's rest. When the bank is disarmed the channel's command and actual value move to the new rest as well.
| o | The bank. |
| ch | Channel index, 0 to OUT_MAX_CHANNELS - 1. |
| role | The new role. |
| unsigned outputs_set_role_channels | ( | outputs_t * | o, |
| out_role_t | role, | ||
| uint8_t | limit, | ||
| uint16_t | command, | ||
| uint32_t | now_ms ) |
Command every channel below limit that carries role, and say how many took it.
One command reaching several channels is what a bench throttle is: the screen has one slider, and the pins it should drive are whichever ones the binding said are motors. Naming them by role rather than by number keeps that decision in the binding, where the operator made it, instead of fixing a channel number in two places that must agree.
Channels of another role are not touched, so a servo bound beside a motor keeps its own command.
| o | The bank. |
| role | Role whose channels are commanded. |
| limit | Channels below this index are considered, clipped at OUT_MAX_CHANNELS. |
| command | Asked position, 0 to OUT_SPAN, as outputs_set() takes it. |
| now_ms | Current time in milliseconds. |
role below limit that were commanded. 0 for NULL. | bool outputs_set_slew | ( | outputs_t * | o, |
| uint8_t | ch, | ||
| uint16_t | per_s ) |
Set how fast a channel moves towards its command.
| o | The bank. |
| ch | Channel index, 0 to OUT_MAX_CHANNELS - 1. |
| per_s | Speed in span units per second; 0 is immediate. |
| bool outputs_slot_driving | ( | const outputs_t * | o, |
| uint8_t | slot ) |
Whether slot is driven: it is configured and the bank is armed for the role of at least one of its channels.
A PPM (pulse-position modulation) frame carries all of its channels or none, so a slot that mixes roles drives every channel when one of them is armed; the others are held at their rest by outputs_step().
| o | The bank. |
| slot | Slot index, 0 to OUT_MAX_SLOTS - 1. |
| void outputs_step | ( | outputs_t * | o, |
| uint32_t | now_ms ) |
Advance slew to now_ms, and put a channel nobody has commanded for timeout_ms at its role's rest.
It decides what a channel renders, not whether it renders: see outputs_driving().
| o | The bank. Nothing is done when NULL. |
| now_ms | Current time in milliseconds. |
|
static |
Does [a0,a0+an) meet [b0,b0+bn)?
| a0 | First index of the first range. |
| an | Length of the first range. |
| b0 | First index of the second range. |
| bn | Length of the second range. |
|
static |
The driver table, indexed by out_driver_t.