Power
This service is experimental and may change in the future.
A power-provider service.
About
Power negotiation protocol
The purpose of the power negotiation is to ensure that there is no more than Iout_hc(max) delivered to the power rail. This is realized by limiting the number of enabled power provider services to one.
Note, that it's also possible to have low-current power providers, which are limited to Iout_lc(max) and do not run a power provider service. These are not accounted for in the power negotiation protocol.
Power providers can have multiple channels, typically multiple Jacdac ports on the provider. Each channel can be limited to Iout_hc(max) separately. In normal operation, the data lines of each channels are connected together. The ground lines are always connected together. Multi-channel power providers are also called powered hubs.
While channels have separate current limits, there's nothing to prevent the user from joining two or more channels outside of the provider using a passive hub. This would allow more than Iout_hc(max) of current to be drawn, resulting in cables or components getting hot and/or malfunctioning. Thus, the power negotiation protocol seeks to detect situations where multiple channels of power provider(s) are bridged together and shut down all but one of the channels involved.
The protocol is built around the power providers periodically sending specially crafted
shutdown
commands in broadcast mode.
Note that this is unusual - services typically only send reports.
The shutdown
commands can be reliably identified based on their first half (more details below).
When a power provider starts receiving a shutdown
command, it needs to take
steps to identify which of its channels the command is coming from.
This is typically realized with analog switches between the data lines of channels.
The delivery of power over the channel which received the shutdown
command is then shut down.
Note that in the case of a single-channel provider, any received shutdown
command will cause a shut down.
A multi-channel provider needs to also identify when a shutdown
command it sent from one channel
is received on any of its other channels and shut down one of the channels involved.
It is also possible to build a data bridge device, with two or more ports.
It passes through all data except for shutdown
commands,
but does not connect the power lines.
Protocol details
The shutdown
commands follow a very narrow format:
- they need to be the only packet in the frame (and thus we can also call them
shutdown
frames) - the second word of
device_id
needs to be set to0xAA_AA_AA_AA
(alternating 0 and 1) - the service index is set to
0x3d
- the CRC is therefore fixed
- therefore, the packet can be recognized by reading the first 8 bytes (total length is 16 bytes)
The exact byte structure of shutdown
command is:
15 59 04 05 5A C9 A4 1F AA AA AA AA 00 3D 80 00
There is one power service per channel. A multi-channel power provider can be implemented as one device with multiple services (typically with one MCU), or many devices with one service each (typically multiple MCUs). The first option is preferred as it fixes the order of channels, but the second option may be cheaper to implement.
After queuing a shutdown
command, the service enters a grace period
until the report has been sent on the wire.
During the grace period incoming shutdown
commands are ignored.
- Upon reset, a power service enables itself, and then only after 0-300ms (random)
sends the first
shutdown
command - Every enabled power service emits
shutdown
commands every 400-600ms (random; first few packets can be even sent more often) - If an enabled power service sees a
shutdown
command from somebody else, it disables itself (unless in grace period) - If a disabled power service sees no
shutdown
command for more than ~1200ms, it enables itself (this is when the previous power source is unplugged or otherwise malfunctions) - If a power service has been disabled for around 10s, it enables itself.
Additionally:
- While the
allowed
register is set to0
, the service will not enable itself (nor sendshutdown
commands) - When a current overdraw is detected, the service stop providing power and enters
Overload
state for around 2 seconds, while still sendingshutdown
commands.
Client notes
If a client hears a shutdown
command it just means it's on a branch of the
network with a (high) power provider.
As clients (brains) typically do not consume much current (as opposed to, say, servos),
the shutdown
commands are typically irrelevant to them.
For power monitoring, the power_status_changed
event (and possibly power_status
register)
can be used.
In particular, user interfaces may alert the user to Overload
status.
The Overprovision
status is generally considered normal (eg. when two multi-channel power providers are linked together).
Server implementation notes
A dedicated MCU per channel
Every channel has:
- a cheap 8-bit MCU (e.g. PMS150C, PFS122),
- a current limiter with FAULT output and ENABLE input, and
- an analog switch.
The MCU here needs at least 4 pins per channel. It is connected to data line of the channel, the control input of the switch, and to the current limiter's FAULT and ENABLE pins.
The switch connects the data line of the channel (JD_DATA_CHx) with the internal shared data bus, common to all channels (JD_DATA_COM). Both the switch and the limiter are controlled by the MCU. A latching circuit is not needed for the limiter because the MCU will detect an overcurrent fault and shut it down immediately.
During reception, after the beginning of shutdown
frame is detected,
the switch is opened for a brief period.
If the shutdown
frame is received correctly, it means it was on MCU's channel.
In the case of only one power delivery channel that's controlled by a dedicated MCU, the analog switch is not necessary.
A shared MCU for multiple channels
Every channel has:
- a current limiter with FAULT output and ENABLE input,
- an analog switch, and
- a wiggle-detection line connecting the MCU to data line of the channel
The MCU again needs at least 4 pins per channel. Switches and limiters are set up like in the configuration above. The Jacdac data line of the MCU is connected to internal data bus.
While a Jacdac packet is being received, the MCU keeps checking if it is a
beginning of the shutdown
frame.
If that is the case, it opens all switches and checks which one(s) of the channel
data lines wiggle (via the wiggle lines; this can be done with EXTI latches).
The one(s) that wiggle received the shutdown
frame and need to be disabled.
Also, while sending the shutdown
frames itself, it needs to be done separately
for each channel, with all the other switches open.
If during that operation we detect wiggle on other channels, then we have detected
a loop, and the respective channels needs to be disabled.
A brain-integrated power supply
Here, there's only one channel of power and we don't have hard real time requirements, so user-programmable brain can control it. There is no need for analog switch or wiggle-detection line, but a current limiter with a latching circuit is needed.
There is nothing special to do during reception of shutdown
packet.
When it is received, the current limiter should just be disabled.
Ideally, exception/hard-fault handlers on the MCU should also disable the current limiter. Similarly, the limiter should be disabled while the MCU is in bootloader mode, or otherwise unaware of the power negotiation protocol.
Rationale for the grace period
Consider the following scenario:
- device A queues
shutdown
command for sending - A receives external
shutdown
packet from B (thus disabling A) - the A
shutdown
command is sent from the queue (thus eventually disabling B)
To avoid that, we make sure that at the precise instant when shutdown
command is sent,
the power is enabled (and thus will stay enabled until another shutdown
command arrives).
This could be achieved by inspecting the enable bit, aftering acquiring the line
and before starting UART transmission, however that would require breaking abstraction layers.
So instead, we never disable the service, while the shutdown
packet is queued.
This may lead to delays in disabling power services, but these should be limited due to the
random nature of the shutdown
packet spacing.
Rationale for timings
The initial 0-300ms delay is set to spread out the shutdown
periods of power services,
to minimize collisions.
The shutdown
periods are randomized 400-600ms, instead of a fixed 500ms used for regular
services, for the same reason.
The 1200ms period is set so that droping two shutdown
packets in a row
from the current provider will not cause power switch, while missing 3 will.
The 50-60s power switch period is arbitrary, but chosen to limit amount of switching between supplies, while keeping it short enough for user to notice any problems such switching may cause.
import { Power } from "@devicescript/core"
const power = new Power()
Commands
shutdown
Sent by the power service periodically, as broadcast.
power.shutdown(): Promise<void>
Registers
enabled
Can be used to completely disable the service.
When allowed, the service may still not be providing power, see
power_status
for the actual current state.
type:
Register<boolean>
(packing formatu8
)read and write
import { Power } from "@devicescript/core"
const power = new Power()
// ...
const value = await power.enabled.read()
await power.enabled.write(value)
- track incoming values
import { Power } from "@devicescript/core"
const power = new Power()
// ...
power.enabled.subscribe(async (value) => {
...
})
write
and read
will block until a server is bound to the client.
maxPower
Limit the power provided by the service. The actual maximum limit will depend on hardware. This field may be read-only in some implementations - you should read it back after setting.
type:
Register<number>
(packing formatu16
)optional: this register may not be implemented
read and write
import { Power } from "@devicescript/core"
const power = new Power()
// ...
const value = await power.maxPower.read()
await power.maxPower.write(value)
- track incoming values
import { Power } from "@devicescript/core"
const power = new Power()
// ...
power.maxPower.subscribe(async (value) => {
...
})
write
and read
will block until a server is bound to the client.
powerStatus
Indicates whether the power provider is currently providing power (Powering
state), and if not, why not.
Overprovision
means there was another power provider, and we stopped not to overprovision the bus.
type:
Register<number>
(packing formatu8
)read only
import { Power } from "@devicescript/core"
const power = new Power()
// ...
const value = await power.powerStatus.read()
- track incoming values
import { Power } from "@devicescript/core"
const power = new Power()
// ...
power.powerStatus.subscribe(async (value) => {
...
})
write
and read
will block until a server is bound to the client.
reading
Present current draw from the bus.
type:
Register<number>
(packing formatu16
)optional: this register may not be implemented
read only
import { Power } from "@devicescript/core"
const power = new Power()
// ...
const value = await power.reading.read()
- track incoming values
import { Power } from "@devicescript/core"
const power = new Power()
// ...
power.reading.subscribe(async (value) => {
...
})
write
and read
will block until a server is bound to the client.
batteryVoltage
Voltage on input.
type:
Register<number>
(packing formatu16
)optional: this register may not be implemented
read only
import { Power } from "@devicescript/core"
const power = new Power()
// ...
const value = await power.batteryVoltage.read()
- track incoming values
import { Power } from "@devicescript/core"
const power = new Power()
// ...
power.batteryVoltage.subscribe(async (value) => {
...
})
write
and read
will block until a server is bound to the client.
batteryCharge
Fraction of charge in the battery.
type:
Register<number>
(packing formatu0.16
)optional: this register may not be implemented
read only
import { Power } from "@devicescript/core"
const power = new Power()
// ...
const value = await power.batteryCharge.read()
- track incoming values
import { Power } from "@devicescript/core"
const power = new Power()
// ...
power.batteryCharge.subscribe(async (value) => {
...
})
write
and read
will block until a server is bound to the client.
batteryCapacity
Energy that can be delivered to the bus when battery is fully charged. This excludes conversion overheads if any.
type:
Register<number>
(packing formatu32
)optional: this register may not be implemented
constant: the register value will not change (until the next reset)
read only
import { Power } from "@devicescript/core"
const power = new Power()
// ...
const value = await power.batteryCapacity.read()
write
and read
will block until a server is bound to the client.
keepOnPulseDuration
Many USB power packs need current to be drawn from time to time to prevent shutdown. This regulates how often and for how long such current is drawn. Typically a 1/8W 22 ohm resistor is used as load. This limits the duty cycle to 10%.
type:
Register<number>
(packing formatu16
)optional: this register may not be implemented
read and write
import { Power } from "@devicescript/core"
const power = new Power()
// ...
const value = await power.keepOnPulseDuration.read()
await power.keepOnPulseDuration.write(value)
- track incoming values
import { Power } from "@devicescript/core"
const power = new Power()
// ...
power.keepOnPulseDuration.subscribe(async (value) => {
...
})
write
and read
will block until a server is bound to the client.
keepOnPulsePeriod
Many USB power packs need current to be drawn from time to time to prevent shutdown. This regulates how often and for how long such current is drawn. Typically a 1/8W 22 ohm resistor is used as load. This limits the duty cycle to 10%.
type:
Register<number>
(packing formatu16
)optional: this register may not be implemented
read and write
import { Power } from "@devicescript/core"
const power = new Power()
// ...
const value = await power.keepOnPulsePeriod.read()
await power.keepOnPulsePeriod.write(value)
- track incoming values
import { Power } from "@devicescript/core"
const power = new Power()
// ...
power.keepOnPulsePeriod.subscribe(async (value) => {
...
})
write
and read
will block until a server is bound to the client.
Events
change
Emitted whenever power_status
changes.
power.change.subscribe(() => {
})