canisterse-fw | ||
canisterse-kicad | ||
common | ||
dispenser-fw | ||
dispenser-kicad | ||
hostbus-fw | ||
hostbus-fw-test | ||
hostbus-kicad | ||
vendors | ||
.gitignore | ||
.gitmodules | ||
Cargo.lock | ||
Cargo.toml | ||
diagram.d2 | ||
diagram.svg | ||
Notes.md | ||
pnet.pcapng | ||
qrgen.sh | ||
README.md |
Functional Requirements
- The NextDispenser requires that up to 500 devices are dispensed at effectively the same time. This makes the round-trip-time (RTT) 2ms max for all devices.
- Communication the super-system (likely PLC) needs to support the the same speed
- Pills need to be dispensed within ~100ms of being requested, the amount dispensed needs to be checked against the request amount to notify of any discrepancy.
- Canisters put onto a NextDispenser need to be uniquely identified to the main system.
- Pills are different sizes/hardness and require different motor settings.
Proposal
NOTE: all specifications are defined for to peak speed (all 500 devices being hit at the same time), likely real-world speed requirements can be significantly lower.
The NextDispenser requires that up to 500 devices are dispensed at effectively the same time. This makes the RTT 2ms max for all devices.
With a synchronous protocol this may mean bauds from 2MHz-16Mhz to have them all done within a second. However to ensure we have lowest latency we propose to use an asynchronous protocol which means you need only the 2Mhz to do it within the 2ms. Internally we'll utilize LVDS/M-LVDS to achieve a speed ceiling of ~100MHz using a common Half-Duplex configuration.
Additionally we will split the bus into multiple buses of ~70 devices meaning that the internal communication has significant headroom.
Communication the super-system (likely PLC) needs to support the the same speed
The host bus controller (which splits the buses) will communicate with the super-system using RS485(to be confirmed?), to ensure we have a consistent RTT, it's efficient bundle requests to multiple nodes into one (which ensures higher throughput): requires >=4Mhz baud.
Pills need to be dispensed within ~100ms of being requested, the amount dispensed needs to be checked against the request amount to notify of any discrepancy.
With the asynchronous protocol the communication overhead at the start is minimal, and so we expect to be able to start within ~1μs. Then it's up to the motor control cycle to get the speed for dispensing.
Canisters put onto a NextDispenser need to be uniquely identified to the main system.
Canisters are planned to identifiabe using a 1-Wire EEPROM such as a DS28E05 (128 bytes, 1k write cycles) or DS28EC20 (2560 bytes, 200k write cycles), with pogo-pins (I/O & GND). Alternatively regular flash/eeprom chips may be used however these require more connectivity (e.g. SCLK, MISO, MOSI, VCC, GND).
Pills are different sizes/hardness and require different motor settings.
These settings are expected to be provided by the main system upon canister identification; if these settings are not provided before attempting to dispense the NextDispenser should alarm.
Technical specifications
- MCUs: RP2040
- Communication: SN65MLVD203B, SP3485
- Motor ICs: DRV8428, INAx181
- Dispensor sensor: VEMT2523SLX01, VSMB2943SLX01
Power Consumption
Measured at 48VDC.
Hostbus:
Idle: 1W
Dispenser:
Idle (Flair-Low + Sensor): 1.3W
Network
PSU: 48V PSU (<=12A)
Switch: GbE Auto-MDI-X Switch\nManaged if possible
PLC: PLC Controller
Unit: {
Hostbus: Hostbus\n(Profinet to 48VDC+LVDS)
DispenserBus: {
Dispenser1: Dispenser1 {}
Dispenser2: Dispenser2 {}
DispenserX: ... {
style.multiple: true
style.stroke-dash: 10
style.stroke: black
style.animated: 1
}
DispenserN: DispenserN {}
}
}
PSU -> Unit.Hostbus {
style: {
stroke: red
}
}
Unit.Hostbus <-> Unit.DispenserBus.Dispenser1 {
style: {
stroke: red
}
}
Unit.Hostbus -> Unit.DispenserBus.Dispenser1 {
style: {
stroke: green
animated: true
}
}
Unit.DispenserBus.Dispenser1 <-> Unit.DispenserBus.Dispenser2 {
style: {
stroke: red
}
}
Unit.DispenserBus.Dispenser1 -> Unit.DispenserBus.Dispenser2 {
style: {
stroke: green
animated: true
}
}
Unit.DispenserBus.Dispenser2 <-> Unit.DispenserBus.DispenserX {
style: {
stroke: red
}
}
Unit.DispenserBus.Dispenser2 -> Unit.DispenserBus.DispenserX {
style: {
stroke: green
animated: true
}
}
Unit.DispenserBus.DispenserX <-> Unit.DispenserBus.DispenserN {
style: {
stroke: red
}
}
Unit.DispenserBus.DispenserX -> Unit.DispenserBus.DispenserN {
style: {
stroke: green
animated: true
}
}
Unit.DispenserBus.DispenserN -> Unit.Hostbus {
style: {
stroke: green
animated: true
}
}
Unit.DispenserBus.DispenserN <-> Unit.Hostbus {
style: {
stroke: red
}
}
Switch -> Unit.Hostbus {
style: {
stroke: green
}
}
PLC -> Switch {
style: {
stroke: green
}
}
flowchart TD
subgraph Network1
subgraph DispensorUnit
MCU[RP2040]
MLVDS[Transciever]
STEPPER-DRV[Motor Driver + Current sense]
LED[SK6812]
MLVDS <-- UART --> MCU
MCU <---> IR-Detector
MCU <---> LED
STEPPER-DRV <---> MCU
end
end
subgraph HostController
direction TB
HostBusA <-- MLVDS/SLAACR --> MLVDS
HostMCU[RP2040?]
HostBusA <--> HostMCU
HostBusB <--> HostMCU
end
HostMCU <-- RS485(?)/Modbus --> PLC
HostBusB <-- MLVDS --> Network2[Network2]
Pre-design ides
RS485 address conflict resolution
SLAACR
(IPv6 SLAAC-inspired) Each node starts with a random address based on their MAC.
Periodically the host will request for each address that they respond (ping or some other command), whenever a node is responding they will delay by a random amount (<0.5ms) and during this time listen for other nodes on the network responding to their address. If they receive a slave response which contains their address they will pick a new address from a list of unseen addresses; if there are no unseen addresses then the unseen table is reset and they will pick a random one.
If for k
req-response cycles they haven't had a conflict they will lock in their address until a power-cycle (maybe not actually that useful?).
Due to the strict timing requirements, responses are deemed to be available at all times (similar to modbus implementations) and the response function should be poll-able after a RX within 0.1ms.
// `decode` mutates addr_table if it contains a response
if let Some(req) = self.decode(rx_ch) && req.addr = self.addr {
// At 5Mhz each bit takes 200ns, so our miss rate would be 1/62.5 (8 bits) @ 0.1ms
// We can increase the marginal probability by changes the wait time (e.g. 0.2 gives 1/125)
await sleep_us(rand()*100);
if rx_ch.has_data() {
if rx.read() == self.addr {
// Detected conflict, pick new address
self.addr = pick_free(addr_table);
} else {
// Someone else responded, what??
}
}
}
Using modbus the first by is the address so we can actually verify that the respondent is in conflict with our address, although it's fairly safe to assume it is whenever it's this close to the master request.
Timing based ordering
In order to get a decent ordering of nodes across the transmission line we can initialize SLAAC with a settle on time-based deterministic addresses.
Once regular SLAAC is in a stable state (ping all nodes a few times); then the master start a timer and sets up an interrupt on the RX pin. The master sends a "prep" command to ensure each node is polling the TX/RX continuously, then it pings each node individually once the first RX bit hits it notes the time it took from command till response (the Round-trip-time), this can be repeated multiple times to improve the estimate; once all nodes are done a large packet is broadcasted with all the changes in addresses (e.g. 10->200, 5->31, 3->5, etc), so that all the addresses are changed at the same time and there are no conflicts. Now all of the nodes should be ordered by the relative position in the transmission line.
The resolution of a single measurement is only (SpeedOfLight × 0.98) / (133 megahertz) ≈ 2.208997059 m assuming perfect accuracy and PIO, however the Pico can be overclock (temporarily) such that it gets 270MHz clock which would give a resolution of ~1m. If we then take multiple measurements per online slave we can take a statistical distance (using kalman filter or similar) which has a higher accuracy, still error in measurement may persist.
Functional
Slave:
- Stepper driver with stuck detection
- Light sensor for pill dispenser
- RS485 driver for remote control (with SLAAC)
- NFC/RFID reader for canister
- LEDs for maintenance and operation info
Host:
- Double RS485/LVDS driver
- Connection with Host? USB, I2C slave? (RS485 | CANBUS)
flowchart TD
subgraph Network1
subgraph DispensorUnit
MCU[RP2040]
RS485-BUS[Transciever: ISO308x]
STEPPER-DRV[Motor: DRV8846?]
NFC-IC[NFC: ST25R3916B]
LED[SK6812]
RS485-BUS <-- UART --> MCU
MCU <-- SPI --> NFC-IC
MCU <---> IR-Detector
MCU <---> LED
STEPPER-DRV <---> MCU
end
end
subgraph HostController
direction TB
HostBusA <-- RS485/MODBUS/SLAACR --> RS485-BUS
HostMCU[RP2040?]
HostBusA <--> HostMCU
HostBusB <--> HostMCU
end
HostMCU <-- (Protocol?) --> PC
HostBusB <-- RS485 --> Network2[Network2]