A useful thing made of a Chinese FX3U clone
Device works over RS-232 (default 115200 8N1), CAN bus (default 250 kbit/s), or MODBUS-RTU (default 9600 8N1).
Full pinout table is available in hardware.c.
The firmware help output also prints the build number and build date from version.inc.
Startup diagnostics
On power-up or reset the firmware prints a startup banner over USART1:
START
IWDGRSTF=1 # reset occurred due to independent watchdog
SFTRSTF=1 # software reset (NVIC_SystemReset)
PORRSTF=1 # power-on / power-down reset
PINRSTF=1 # reset via NRST pin
Only the flags that were actually set are printed. After printing, all reset flags are cleared.
Hardware notes
Inputs (X)
X8 is not a screw terminal — it is the on-board "Prog" pushbutton (PB2).
X9 is absent. Bits of the inchannels mask correspond to these positions.
| Ch | Pin | Notes |
|---|---|---|
| X0 | PB13 | |
| X1 | PB14 | |
| X2 | PB11 | |
| X3 | PB12 | |
| X4 | PE15 | |
| X5 | PB10 | |
| X6 | PE13 | |
| X7 | PE14 | |
| X8 | PB2 | on-board "Prog" button |
| X9 | — | absent |
| X10 | PE11 | |
| X11 | PE12 | |
| X12 | PE9 | |
| X13 | PE10 | |
| X14 | PE7 | |
| X15 | PE8 |
Outputs (Y)
Y8 and Y9 are absent.
| Ch | Pin |
|---|---|
| Y0 | PC9 |
| Y1 | PC8 |
| Y2 | PA8 |
| Y3 | PA0 |
| Y4 | PB3 |
| Y5 | PD12 |
| Y6 | PB15 |
| Y7 | PA7 |
| Y8 | — |
| Y9 | — |
| Y10 | PA6 |
| Y11 | PA2 |
On-board LED
"RUN" LED is on PD10. Active low: led returns the logical state (1 when the LED is on,
0 when off).
ADC channels
| № | Enum | Pin / source | Meaning |
|---|---|---|---|
| 0 | ADC_CH_0 |
PA1 / adc1 | voltage input, up to 11 V |
| 1 | ADC_CH_1 |
PA3 / adc3 | voltage input, up to 11 V |
| 2 | ADC_CH_2 |
PC4 / adc14 | voltage input |
| 3 | ADC_CH_3 |
PC5 / adc15 | current input |
| 4 | ADC_CH_4 |
PC0 / adc10 | current input, 0..20 mA |
| 5 | ADC_CH_5 |
PC1 / adc11 | current input |
| 6 | ADC_POT0 |
PC2 / adc12 | right on-board potentiometer |
| 7 | ADC_POT1 |
PC3 / adc13 | left on-board potentiometer |
| 8 | ADC_CH_TSEN |
internal | MCU temperature sensor |
| 9 | ADC_CH_VDD |
internal | Vdd reference |
Each channel is sampled continuously in scan mode via DMA into a circular buffer of 9 × ADC_CHANNELS values. The getter returns the median of the last 9 samples per channel. Reported raw
values are 12-bit (0..4095).
The mcutemp command returns MCU temperature in °C × 10 (as int32_t).
Runtime parameters
Watchdog
IWDG prescaler /4 (LSI ≈ 40 kHz) with reload 1250 → about 125 ms watchdog timeout. Refreshed
in every main-loop iteration, in DMA/send wait loops and during flash writes. Any hang longer than
that triggers a reset.
CAN timeouts
- Mailbox wait inside
CAN_send():SEND_TIMEOUT_MS / 10= 10 ms. - High-level send loops (command reply, ESW notifications): up to
SEND_TIMEOUT_MS= 100 ms. If a message cannot be queued,error=canbusyis printed to USART.
Buffer sizes
| Subsystem | Macro | Value | Notes |
|---|---|---|---|
| USART input | UARTBUFSZI |
196 | longer lines are dropped; firmware prints USART IN buffer overflow! |
| USART output | UARTBUFSZO |
256 | |
| CAN RX queue | CAN_INMESSAGE_SIZE |
8 | extra messages are dropped silently |
| Modbus RX | MODBUSBUFSZI |
68 | |
| Modbus TX | MODBUSBUFSZO |
64 |
Serial protocol
Every command is a string terminated by \n. General syntax:
command[number][=value]
command— command name (letters/digits);number— optional parameter number, 0..127;=value— optional setter value.
Numbers are parsed by getnum() and accept decimal (123), hexadecimal (0x7B), octal (0173)
and binary (0b1111011). Signed values (getint()) allow a leading -.
Values in parentheses after a flag command is its bit number in the whole uint32_t. E.g. to reset
flag f_relay_inverted you can call f_relay_inverted=0 or flags2=0.
commands format: parameter[number][=setter]
parameter [CAN idx] - help
--------------------------
CAN bus commands:
canbuserr - print all CAN bus errors (a lot of if not connected)
cansniff - switch CAN sniffer mode
s - send CAN message: ID 0..8 data bytes
Configuration:
bounce [14] - set/get anti-bounce timeout (ms, max: 1000)
canid [6] - set both (in/out) CAN ID / get in CAN ID
canidin [7] - get/set input CAN ID
canidout [8] - get/set output CAN ID
canspeed [5] - get/set CAN speed (bps)
dumpconf - dump current configuration
eraseflash [10] - erase all flash storage
f_relay_inverted (2) - inverted state between relay and inputs
f_send_esw_can (0) - change of IN will send status over CAN with `canidin`
f_send_relay_can (1) - change of IN will send also CAN command to change OUT with `canidout`
f_send_relay_modbus (3) - change of IN will send also MODBUS command to change OUT with `modbusidout` (only for master!)
flags [17] - set/get configuration flags (as one U32 without parameter or Nth bit with)
modbusid [20] - set/get modbus slave ID (1..247) or set it master (0)
modbusidout [21] - set/get modbus slave ID (0..247) to send relay commands
modbusspeed [22] - set/get modbus speed (1200..115200)
saveconf [9] - save configuration
usartspeed [15] - get/set USART1 speed
IN/OUT:
adc [4] - get raw ADC value for the given channel (0..9)
esw [12] - anti-bounce read inputs
eswnow [13] - read current inputs' state
led [16] - work with onboard LED
relay [11] - get/set relay state (0 - off, 1 - on)
Other commands:
inchannels [18] - get u32 with bits set on supported IN channels
mcutemp [3] - get MCU temperature (*10degrC)
modbus - send modbus request with format "slaveID fcode regaddr nregs [N data]", to send zeros you can omit rest of 'data'
modbusraw - send RAW modbus request (will send up to 62 bytes + calculated CRC)
outchannels [19] - get u32 with bits set on supported OUT channels
reset [1] - reset MCU
time [2] - get/set time (1ms, 32bit)
wdtest - test watchdog
Value in square brackets is the CAN bus command code (see below).
Notes on specific commands
bounce
bouncetime (default 50 ms) is not a classical debounce delay — it is the per-input sampling
interval. Once an input has been sampled, it will not be re-read until bouncetime milliseconds
have elapsed. Worst-case reaction time for an input change is therefore up to bouncetime ms;
changes within that window are ignored (which is the debounce behaviour itself).
s
Send a CAN message. All numbers are space-separated; the first is the CAN ID (0..0x7FF), the
remaining 0..8 numbers are data bytes. Numbers may be given in any format accepted by getnum().
s 0x123 0x11 0x22 0x33
s 291 1 2 3 4
On invalid arguments the firmware prints error=badpar / error=badval / error=wronglen and
sends nothing.
cansniff
When enabled, every received CAN frame is printed to USART in the format:
<time_ms> #<ID> <b0> <b1> ...
All fields are hexadecimal except <time_ms>. While messages are being received, the regular
periodic USART keep-alive is suppressed.
adc
Accepts a parameter number 0..9 (ADC_CHANNELS - 1). Numbers outside this range return
error=badpar.
flags
With a parameter number (0..MAX_FLAG_BITNO = 0..3): sets/reads the Nth bit only. Without a
parameter ("no par", 0x7F): operates on the whole uint32_t. Bits above MAX_FLAG_BITNO return
error=badpar.
Default configuration
Flash storage is empty after flashing; on first boot flashstorage_init() returns currentconfidx = -1 and the firmware uses USERCONF_INITIALIZER from flash.c:
CANspeed = 250000
CANIDin = 1
CANIDout = 2
usartspeed = 115200
bouncetime = 50
modbusID = 1
modbusIDout = 2
modbusspeed = 9600
flags = { sw_send_relay_inv = 1 }
After the first saveconf, these defaults are replaced by the stored record.
CAN bus protocol
Default speed is 250 kbit/s. Default CAN IDs are 1 (input) and 2 (output) for a slave. All multi-byte data is little-endian.
| Byte(s) | Meaning |
|---|---|
| 0, 1 | uint16_t command code (see table below) |
| 2 | uint8_t parameter number: 0..126, ORed with 0x80 for setter, 127 = "no parameter" |
| 3 | uint8_t error code (only in device answers) |
| 4..7 | int32_t data |
When the device receives a CAN packet addressed to its own ID or to ID = 0 ("broadcast"), it performs the requested action and sends an answer (usually a getter reply). If the command cannot be executed or carries bad data, the device returns the same packet with the error code inserted into byte 3.
Getters may be requested by a 3-byte packet (command code + parameter). "No parameter" (0x7F) in some commands means "all data" — e.g. get/set all relays or get all inputs.
CAN bus error codes (byte 3 of the answer)
| Code | Name | Meaning |
|---|---|---|
| 0 | ERR_OK |
all OK |
| 1 | ERR_BADPAR |
wrong parameter |
| 2 | ERR_BADVAL |
value out of range |
| 3 | ERR_WRONGLEN |
wrong message length (for setter or where a parameter is required) |
| 4 | ERR_BADCMD |
unknown command code |
| 5 | ERR_CANTRUN |
cannot run the command (bad parameters or other reason) |
Bus-level errors (stuff/form/ack/bit/CRC, bus-off, error-passive, error-warning) are not reported
in byte 3. They are printed over USART by CAN_printerr() when the canbuserr printer is enabled:
Receive error counter: <n>
Transmit error counter: <n>
Last error code: <name>
[Bus off] [Passive error limit] [Error counter limit]
CAN command codes
| Code | Enum | Text command |
|---|---|---|
| 0 | CMD_PING |
(ping) |
| 1 | CMD_RESET |
reset |
| 2 | CMD_TIME |
time |
| 3 | CMD_MCUTEMP |
mcutemp |
| 4 | CMD_ADCRAW |
adc |
| 5 | CMD_CANSPEED |
canspeed |
| 6 | CMD_CANID |
canid |
| 7 | CMD_CANIDin |
canidin |
| 8 | CMD_CANIDout |
canidout |
| 9 | CMD_SAVECONF |
saveconf |
| 10 | CMD_ERASESTOR |
eraseflash |
| 11 | CMD_RELAY |
relay |
| 12 | CMD_GETESW |
esw |
| 13 | CMD_GETESWNOW |
eswnow |
| 14 | CMD_BOUNCE |
bounce |
| 15 | CMD_USARTSPEED |
usartspeed |
| 16 | CMD_LED |
led |
| 17 | CMD_FLAGS |
flags |
| 18 | CMD_INCHNLS |
inchannels |
| 19 | CMD_OUTCHNLS |
outchannels |
| 20 | CMD_MODBUSID |
modbusid |
| 21 | CMD_MODBUSIDOUT |
modbusidout |
| 22 | CMD_MODBUSSPEED |
modbusspeed |
Examples
All data in hex. Slave ID is omitted.
Get current time:
- request:
02 00 00 - answer:
02 00 00 00 de ad be ef— last four bytes are time in ms since power-up.
Set relay number 5:
- request:
0b 00 85 00 01 00 00 00 - answer:
0b 00 05 00 01 00 00 00
Set relays 0..3, reset the rest:
- request:
0b 00 ff 00 07 00 00 00 - answer:
0b 00 7f 00 07 00 00 00
Changing flags works like the text command: with a parameter number the Nth bit is changed, without
a parameter the whole uint32_t is replaced.
MODBUS-RTU protocol
The device can operate as master or slave. Default format is 9600-8N1. Big-endian, as the standard requires. Default slave ID is 1, and the "relay command" target ID is 2.
Set modbusid=0 to enter master mode. In master mode the device no longer answers incoming modbus
requests, but instead parses incoming responses and prints them to USART.
The modbus command sends a formal modbus request in the format:
modbus = slaveID fcode regaddr nregs [N data]
All numbers are space-separated and parsed by getnum() (decimal / hex / octal / binary).
slaveID and fcode are one byte each; regaddr and nregs are two bytes little-endian; N is
one byte; data is N bytes. Optional data bytes are allowed only for "multiple" functions (0x0F,
0x10). For simple setters (0x05, 0x06) nregs is the two-byte value written to the slave.
modbus = 1 6 2 1 # slave 1, write register, register 2 (MR_LED), value 1
modbus = 1 0x0f 0 8 1 0xff # slave 1, write coils, 8 coils, 1 byte of data
modbusraw does not validate the fields; it just sends the data (user should add CRC by himself).
Useful for testing unusual requests.
In master mode, flag f_send_relay_modbus makes the device send an "write coils" command with ID =
modbusidout every time the IN state changes. This lets you bind several devices: inputs of one
drive the outputs of another. If modbusidout is zero, a broadcast is sent (slaves do not reply to
broadcasts, they just perform the action).
Implementation notes
- Modbus uses UART4 with DMA for both RX and TX. End of frame is detected by the IDLE interrupt.
- There is no 3.5-character silent-interval handling — any IDLE marks the end of a packet.
- Input buffer is 68 bytes (up to 67 data bytes), output buffer is 64 bytes (up to 64 bytes).
Enlarge
MODBUSBUFSZI/MODBUSBUFSZOinmodbusrtu.hif needed. - Maximal modbus slave ID is 247.
- The device does not reply to broadcast requests (ID = 0).
Slave registers
Holding registers: [R] = read-only, [W] = write-only, [RW] = read/write.
| № | Symbol | Access | Meaning |
|---|---|---|---|
| 0 | MR_RESET |
W | reset MCU |
| 1 | MR_TIME |
RW | MCU time in ms (uint32_t) |
| 2 | MR_LED |
RW | on-board LED state |
| 3 | MR_INCHANNELS |
R | uint32_t of available IN channels |
| 4 | MR_OUTCHANNELS |
R | uint32_t of available OUT channels |
Supported function codes
01 — read coils
Read state of all relays. regaddr must be 0, nregs must be a multiple of 8 (in this hardware: 8 or 16). Answer contains nregs / 8 bytes; bit 0 of the first data byte is relay 0.
Example — read all relays; only relay 10 active:
- request:
01 01 00 00 00 10 - answer:
01 01 02 00 04
Errors: 02 — non-zero regaddr; 03 — nregs not a multiple of 8 or too large.
02 — read discrete inputs
Same semantics as "read coils", but for the IN channels.
Example — read first 8 INs; all 4 low-order inputs active:
- request:
01 02 00 00 00 08 - answer:
01 02 01 0f
03 — read holding register
Reads one register at a time.
Example — read time:
- request:
01 03 00 01 00 01 - answer:
01 03 04 01 53 15 00— value0x00155301= 1397505 ms ≈ 1397.5 s.
Errors: 02 — bad regaddr; 03 — regno != 1.
04 — read input register
Read nregs ADC channels starting at regaddr.
Example — read channels 5..8:
- request:
01 04 00 05 00 04 - answer:
01 04 08 6c 08 21 00 33 00 41 00—0x086c(2156) for channel 5, etc.
Errors: 02 — bad start channel; 03 — bad amount (zero or beyond last channel).
05 — write coil
Changes a single relay state. regaddr — relay number, nregs — value (0 = off, non-zero = on).
Example - turn on coil 3:
- request:
01 05 00 03 00 01 - answer:
01 05 00 03 00 01
Errors: 02 — bad relay number.
06 — write holding register
Writes to one register (MR_RESET, MR_TIME or MR_LED).
Example — turn LED on:
- request:
01 06 00 02 00 01 - answer:
01 06 00 02 00 01
Errors: 02 — bad register.
0F — write multiple coils
Changes all relays at once. regaddr must be 0, nregs a multiple of 8, N = (nregs + 7) / 8.
Each data bit is a relay state.
Example — turn on relays 0..7:
- request:
01 0f 00 00 00 08 01 ff - answer:
01 0f 00 00 00 08
Turn on all relays (0..7 and 10, 11):
- request:
01 0f 00 00 00 10 02 ff 0f - answer:
01 0f 00 00 00 10 56 c2
Errors: 02 — non-zero regaddr; 03 — wrong amount; 07 — cannot change relays.
10 — write multiple registers
Only MR_TIME can be written this way; nregs must be 1 and the data length 4 bytes.
Example — clear Tms:
- request:
01 10 00 01 00 01 04 00 00 00 00 - answer:
01 10 00 01 00 01
Errors: 02 — wrong register.
Modbus exception codes
| Code | Name | Meaning |
|---|---|---|
| 01 | ME_ILLEGAL_FUNCION |
function code is not authorized for the slave |
| 02 | ME_ILLEGAL_ADDRESS |
data address is not authorized |
| 03 | ME_ILLEGAL_VALUE |
data field value is not authorized |
| 04 | ME_SLAVE_FAILURE |
unrecoverable error |
| 05 | ME_ACK |
accepted, but processing takes a long time |
| 06 | ME_SLAVE_BUSY |
slave is busy |
| 07 | ME_NACK |
programming request cannot be performed |
| 08 | ME_PARITY_ERROR |
memory parity error |
Limitations
- CAN RX queue holds 8 messages; extras are dropped silently.
- USART RX buffer holds 196 bytes; longer lines are discarded with an error message.
- Modbus does not implement the 3.5-character silent interval. Buffers are 67/64 bytes.
- The Modbus master does not implement retries, timeouts or a transaction queue — it just prints incoming responses. Reliable exchange should be arranged by the host.
- CAN filters accept only the configured
CANIDinplus ID 0 (broadcast); the "monitor" mode adds a second, match-all filter. - The IWDG is enabled in release builds. Any hang longer than ~125 ms triggers a reset.
- The
EBUGbuild disables the IWDG and enables verboseDBG(...)messages.
Short programming guide
Adding a new value to flash storage
All stored values are described in struct user_conf (flash.h). You can add new fields, but keep
32-bit alignment in mind. Bit flags live in union confflags_t, which combines 32-bit and per-bit
access.
After adding a field:
- Add a setter/getter (usually via
u32setgetorflagsetget). - Add a line in
dumpconf()(proto.c).
The text protocol allows working with flags by their semantic name. To add a flag, edit proto.c:
- add a
static const char* S_f_...constant with the flag name; - add its address to the
bitfields[]array in the same order as the bits are defined inconfflags_t(critical:dumpconfandconfflagsindex this array by bit number); - add an entry to the
text_cmdenum; - add a
funcdescrentry tofunclist; - modify
confflags()for setter/getter handling.
Adding a new command
Base commands are processed in canproto.c and proto.c. modbusproto.c handles modbus-specific
commands.
To add a CAN/serial command:
- Add an enum member in
canproto.h(CMD_...). This value is the numeric command code on the wire. - Add a string constant with the text command name in
proto.c. - Add a
funcdescrentry tofunclist. - Implement the handler in
canproto.c(returns one oferrcodes, receives aCAN_message *).
Important: the funclist[] array in canproto.c is indexed by enum value — the entry for
CMD_X must be at array position CMD_X. Use designated initializers ([CMD_X] = {...}) as the
existing code does.
The commonfunction struct has fields {fn, minval, maxval, datalen}:
minval == maxvaldisables range checking of the value (bytes 4..7) for setter commands;datalenis the minimal packet length in bytes that the handler requires.
The handler only sees a CAN_message *. The serial parser builds an equivalent packet from user
input: [C C P 0 V0 V1 V2 V3], where C = command code (little-endian); P = parameter number
(or 0x7F if not specified), ORed with 0x80 in case of a setter; Vx = bytes of the user value
(little-endian).
For uint32_t configuration values use u32setget; for bit flags — flagsetget.
Adding a serial-only command
If the command has no CAN equivalent, work purely in proto.c:
- add an entry to the
text_cmdenum (negative indices are used infunclist); - add a string constant and a
funcdescrentry; - implement the handler with signature
errcodes fn(const char *str, text_cmd cmd); - register it in the
textfunctions[]array.
Working with modbus
Modbus-specific enums (modbus_fcode, modbus_exceptions) and structs (modbus_request,
modbus_response) are declared in modbusrtu.h. data fields hold bytes in wire order. For
requests without data (Fcode ≤ 6), data may be NULL.
High-level modbus slave handlers live in modbusproto.c. To add a new register, extend the
modbus_registers enum in modbusproto.h and handle the new value in readreg(), writereg() or
writeregs(). The main dispatch point is parse_modbus_request().
License
All source files are licensed under GNU General Public License v3.0 unless stated otherwise.