Porting¶
Run the driver on a new MCU by writing one class: the serial interface.
The driver never calls your SDK directly. It calls three methods on a class you derive from
alicat_basis2::UartInterface<Self> (in
inc/alicat_basis2_uart_interface.hpp). The base class uses CRTP: the
calls are resolved at compile time, so there are no virtual functions, no vtable and nothing to allocate.
Figure description and sources
From top to bottom. Your application holds one Driver
Drawn from: inc/alicat_basis2.hpp Driver members; inc/alicat_basis2_modbus.hpp frame builders; inc/alicat_basis2_uart_interface.hpp UartInterface; figure module docs/diagrams/figures/driver-layers.py.
What you implement¶
| Method | Required | Contract |
|---|---|---|
write(const uint8_t* data, std::size_t length) |
yes | Send all length bytes; return once they are accepted by the UART (better: on the wire) |
read(uint8_t* out, std::size_t max, uint32_t timeout_ms) |
yes | Return when max bytes arrived or timeout_ms passed; the return value is the number of bytes in out |
flush_rx() |
yes | Discard every byte already received |
delay_ms_impl(uint32_t ms) |
optional | Sleep at least ms; UartInterface::delay_ms() calls it when present and does nothing otherwise (there is no portable time base for a busy-wait) |
log_impl(const char* tag, const char* fmt, ...) |
optional | printf-style log line; UartInterface::log() forwards the tag, format and arguments to it when present and does nothing otherwise |
The base class finds the optional hooks at compile time, public or private (with
friend class alicat_basis2::UartInterface<Self>;). The driver logs every failed transaction through log()
with the tag alicat_basis2, for example addr 1 fn 0x06 reg 514: ModbusException IllegalDataAddress; the
address sweep stays quiet.
The driver calls flush_rx(), then write(), then read() twice for every request: once for the first 3 bytes
of the reply, then for the rest. The first 3 bytes tell a normal reply from a 5-byte exception reply (bit 7 of the
function code), so the second read() asks for exactly the bytes still to come.
The port¶
class MyUart : public alicat_basis2::UartInterface<MyUart> {
public:
// Send a whole Modbus request; return once the last byte is on the wire.
void write(const uint8_t* data, std::size_t length) noexcept {
platform_uart_write(data, length);
}
// Wait for up to `max` bytes; return early only when all of them arrived.
std::size_t read(uint8_t* out, std::size_t max, uint32_t timeout_ms) noexcept {
return platform_uart_read(out, max, timeout_ms);
}
// Drop stale bytes (a late reply, line noise) before the next request.
void flush_rx() noexcept { platform_uart_flush_rx(); }
// Optional: a real sleep (at least `ms`) for UartInterface::delay_ms().
void delay_ms_impl(uint32_t ms) noexcept { platform_delay_ms(ms); }
// Optional: where the driver's failure logs go (printf-style).
void log_impl(const char* tag, const char* fmt, ...) noexcept {
va_list ap;
va_start(ap, fmt);
platform_log(tag, fmt, ap);
va_end(ap);
}
};
Rules for the transfer¶
- Wait for every byte asked for. The driver asks for exactly the bytes it needs (3, then 2 for an exception
reply, 5 for a write echo or 2 + 2N for a read of N registers). A
read()that returns after the first chunk gives the driver a partial frame:InvalidFrameorBadCrcthat come and go. - Return what you have at the timeout. The return value must be the number of bytes that arrived;
0is reported asTimeout. - Give the driver a sleep for the 3.5-character gap. A new request must start at least 3.5 characters
(1.75 ms above 19200 baud) after the previous frame ended (Hardware). The driver waits
that long through
delay_ms()before every request, sodelay_ms_impl()must sleep at leastms: a tick-based sleep that rounds 2 ms down to 0 ticks leaves no gap. Withoutdelay_ms_impl(), wait inwrite()or let the UART do it. Tell the driver the baud rate withSetBusBaudRate()when it is not 38400. - Turn the bus around. On RS-485, assert DE/RE for the request and release it right after the last stop bit,
or the instrument's reply collides with your driver. Prefer the UART's hardware RS-485 mode (ESP-IDF
UART_MODE_RS485_HALF_DUPLEX, STM32DEM) over a GPIO toggled in software. - 8N1 at the instrument's baud rate. 38400 out of the factory.
Reference implementation¶
The ESP32 examples define EspUartAdapter in
alicat_basis2_minimal_example.cpp: ESP-IDF
uart_write_bytes(), uart_read_bytes() (which returns at max bytes or the timeout), uart_flush_input() and
vTaskDelay(), with UART1 in RS-485 half-duplex mode. Start from it for any ESP-IDF target.
Check your port¶
- Build it: the port on this page is compiled by the host CI from
tests/docs/, with the driver instantiated over it. - Call
ReadIdentity(): a serial number that matches the label proves the whole path. - Call
ReadInstantaneous()in a loop for a minute: noBadCrcorInvalidFrameon a healthy link. - Call a write the instrument rejects (for example
SetCommWatchdogMs()on a meter): it should returnModbusExceptionat once, withGetLastException()=IllegalDataAddress, not after the timeout.
Threads¶
The driver is not thread-safe. Give the bus one owner task, or guard every call on every driver that shares a port with one mutex. Instruments on separate UARTs can run in parallel.