|
rcbench
Servo and ESC test bench: panel, IOmcu and shared C
|
The intermediary every output goes through: one set of rules for arming, clamping, slew and the silence timeout, with a protocol as a driver behind them. More...
#include <stdbool.h>#include <stdint.h>Go to the source code of this file.
Data Structures | |
| struct | out_driver_def_t |
| What a driver is shaped like. More... | |
| struct | out_channel_t |
| The state of one channel of the bank. More... | |
| struct | out_slot_t |
| One driver configuration: the channels and pin it renders. More... | |
| struct | outputs_t |
| The state of a bank of channels and slots. More... | |
Macros | |
| #define | OUT_SPAN 1000u |
| A command runs 0..OUT_SPAN across the channel's own travel. | |
| #define | OUT_MAX_CHANNELS 16u |
| Channels in a bank. | |
| #define | OUT_MAX_SLOTS 8u |
| Slots in a bank: driver configurations, each rendering channels. | |
| #define | OUT_FLOOR_US 400u |
| What a pulse driver may render, in microseconds. | |
| #define | OUT_CEILING_US 2500u |
| #define | OUT_ROLE_BIT(r) |
| A set of roles, one bit a role: what an arm covers. | |
| #define | OUT_ROLES_ALL |
| Every role: the bits of all of them. | |
| #define | OUT_MAX_PIN 63u |
| The highest pin number a reservation mask can hold. | |
| #define | OUT_DEFAULT_TIMEOUT_MS 500u |
| Silence after which a channel is rendered at its role's rest. It keeps driving; see outputs_driving(). | |
Enumerations | |
| enum | out_role_t { OUT_ROLE_THROTTLE = 0 , OUT_ROLE_SURFACE = 1 } |
| What rest means for a channel, which is the one thing a command cannot say. More... | |
| enum | out_driver_t { OUT_DRIVER_NONE = 0 , OUT_DRIVER_PWM , OUT_DRIVER_PPM , OUT_DRIVER_DSHOT , OUT_DRIVER_DSHOT_BIDIR , OUT_DRIVER_COUNT } |
| The drivers a slot can be configured as. 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. | |
| bool | outputs_configure (outputs_t *o, uint8_t slot, const out_slot_t *cfg) |
| Configure one slot, or clear it with OUT_DRIVER_NONE. | |
| 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. | |
| 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. | |
| void | outputs_channels_apply_n (outputs_t *o, const uint16_t *regs, uint8_t first, uint8_t count, uint32_t now_ms) |
Apply count channels of a CHANNELS page from first, as commands. | |
| 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. | |
| 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? | |
| 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_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. | |
| void | outputs_all_off (outputs_t *o) |
| Everything to rest, immediately. | |
| 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. | |
The intermediary every output goes through: one set of rules for arming, clamping, slew and the silence timeout, with a protocol as a driver behind them.
A channel is a normalised command and a role. Screens and procedures write channels and do not know what is on the wire.
A driver declares its shape: how many channels one of it consumes, how many pins, and whether the pin is also an input. PPM (pulse-position modulation) is eight channels on one pin, and bidirectional DShot listens on the pin it drives.
Arming, clamping, slew and where to go when the link goes quiet are answered here for every output. Only turning a command into edges is per-protocol, on the IOmcu.
The unit is a thousandth of the channel's own span. Microseconds mean nothing to DShot and a DShot code means nothing to a PWM (pulse-width modulation) servo; both have a proportion of travel, and 1000 steps is finer than any of these protocols resolve.
Refused and clamped are different answers. An endpoint outside 400 to 2500us is refused: it is a configuration mistake, and accepting it leaves the range looking set when it is not. A command outside its span is clamped: commands arrive many times a second from a host that may be mid-drag, and an output that stops at its stop is what a stop is for.
SPDX-License-Identifier: MIT
| #define OUT_FLOOR_US 400u |
What a pulse driver may render, in microseconds.
Endpoints outside this are refused: no servo made takes them, so they are a typo, not a range. The floor is below a 760us tail servo's end of travel, 410us.
| #define OUT_ROLE_BIT | ( | r | ) |
A set of roles, one bit a role: what an arm covers.
| #define OUT_ROLES_ALL |
Every role: the bits of all of them.
| enum out_driver_t |
The drivers a slot can be configured as.
| enum out_role_t |
What rest means for a channel, which is the one thing a command cannot say.
A throttle at rest is stopped, and its slew is asymmetric: going up is ramped and coming down is immediate, because reducing throttle is the safe direction. A surface at rest is centred, and its slew is symmetric, because for a surface neither direction is the safe one.
| Enumerator | |
|---|---|
| OUT_ROLE_THROTTLE | rests at 0 |
| OUT_ROLE_SURFACE | rests at OUT_SPAN / 2 |
| 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. |
| void outputs_channels_apply_n | ( | outputs_t * | o, |
| const uint16_t * | regs, | ||
| uint8_t | first, | ||
| uint8_t | count, | ||
| uint32_t | now_ms ) |
Apply count channels of a CHANNELS page from first, as commands.
A page write arrives in frames and each frame says which registers it carries, so only those are commanded: a channel outside the frame has not been commanded by it, and stamping its clock would keep it alive on somebody else's traffic. That is the whole of the per-channel timeout – a servo held at 10Hz must not keep a surface nobody is driving from reaching its rest.
| o | The bank. |
| regs | The CHANNELS page, indexed by channel number. |
| first | First channel to command, below LINK_CH_COUNT. |
| count | Number of channels from first. Clipped at LINK_CH_COUNT. |
| now_ms | Current time in milliseconds, stamped on each channel. |
| 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. |