9.2 KiB
Modbus Range Limiter Test
Purpose
Verify that LimitModbusRange() in the firmware works correctly: when a value outside the allowed range is written to a holding register, the value read back after a config rewrite must be clamped to the defined min or max.
The script supports both device types and auto-detects via holding register 163 (HARDWARE_REV_HI):
0x0021→ TPU (fenghuang-dsp)0x0022→ RPU (garuda-dsp)
Rewrite Trigger Mechanism
RPU (garuda-dsp)
Write the MAIN_FORCE_NORMAL_MODE password to two consecutive holding registers:
- Addr 48 (
MODBUS_FORCE_PASSWORD_LO) =0xFEED - Addr 49 (
MODBUS_FORCE_PASSWORD_HI) =0xCEE5
Firmware detects the password → calls modbus_to_config() (which includes LimitModbusRange()) → write_all_flash() → soft_reset().
TPU (fenghuang-dsp)
Write value 1 to holding coil bit 49 (MODBUS_ALL_CFG_CMD_BIT) via FC5.
In the main loop, update_modbus_input() detects write_flash == 1 → calls modbus_to_config() (which includes LimitModbusRange()) → write_all_flash() → soft_reset().
Note: Both devices call
DINTinsidewrite_all_flash()before writing to flash, which means the device cannot send an ACK for the trigger write.NoResponseErroron the trigger write is expected and indicates the rewrite was successfully initiated. The script polls until the device finishes booting (up toREWRITE_WAITseconds).
Registers Under Test
All registers that appear inside LimitModbusRange() are tested, including:
uint16/int16registers clamped byLimitUpLoUint16()/LimitUpLoInt16()float32registers clamped byLimitUpLoFloat()— stored as two consecutive holding registers viaEndianConvert32Bits(big-endian word order). Read with FC3 / written with FC16.
For TPU float32 calibration registers the allowed range is a fixed percentage of the nominal constant from global_variables.h:
- Gain (A):
[nominal × 0.9, nominal × 1.1] - Offset (B) with positive nominal:
[nominal × 0.8, nominal × 1.2] - Offset (B) with negative nominal: limits are computed the same way but the upper and lower bounds are numerically inverted (firmware stores them in the same order regardless of sign).
PFC_current_rms_cali_Bis excluded because its nominal is0.0(limits collapse to[0, 0]).
Float readback comparison uses a relative tolerance of FLOAT_EPSILON = 1e-4 (0.01%) to account for IEEE 754 rounding.
Limit sources:
- RPU:
garuda-dsp/app/app_modbus/app_modbus.h,global_variables.h - TPU:
fenghuang-dsp/app/app_modbus/app_modbus.h,global_variables.h,sec_control.h,fan.h,app_inv.h
Test Procedure (batch approach)
- Phase 0 — Read and save all current default values.
- Phase 1 — Write all below-min values to their registers → single trigger → read all back.
- PASS if readback ==
min_val - FAIL if readback !=
min_val
- PASS if readback ==
- Phase 2 — Write all above-max values to their registers → single trigger → read all back.
- PASS if readback ==
max_val - FAIL if readback !=
max_val
- PASS if readback ==
- Restore — Write all default values back → trigger rewrite to restore flash to its original state.
The batch approach (write all, then trigger once) is used instead of triggering per register to minimise total reboot wait time.
CSV Report
File is named <device>_modbus_limiter_report_<timestamp>.csv and saved in the same directory as the script.
| Column | Content |
|---|---|
| 1 | Register name |
| 2 | Data type (uint16 / int16 / float32) |
| 3 | Original default value |
| 4 | Allowed minimum value |
| 5 | Allowed maximum value |
| 6 | Value written below min |
| 7 | Readback after rewrite (below-min) |
| 8 | Result: PASS / FAIL / ERR / N/A |
| 9 | Value written above max |
| 10 | Readback after rewrite (above-max) |
| 11 | Result: PASS / FAIL / ERR / N/A |
N/A = not applicable (e.g. uint16 with min=0 cannot go below zero, or int16 with max=32767).
ERR = Modbus read/write error.
Environment Requirements
- Python 3.10+
minimalmodbus(pip install minimalmodbus)- RS-485 / USB-to-serial connection to the device (RPU or TPU)
- Slave ID:
0x10(16), Baud: 9600, Mode: RTU
Kiểm tra Modbus Range Limiter (Tiếng Việt)
Mục đích
Kiểm tra hàm LimitModbusRange() trong firmware có hoạt động đúng không: khi người dùng ghi giá trị vượt ngoài ngưỡng cho phép vào holding register, sau khi firmware thực hiện rewrite config, giá trị đọc lại phải bị kẹp (clamp) đúng về min hoặc max đã định nghĩa.
Script hỗ trợ cả hai loại thiết bị và tự động nhận dạng thông qua holding register 163 (HARDWARE_REV_HI):
0x0021→ TPU (fenghuang-dsp)0x0022→ RPU (garuda-dsp)
Cơ chế trigger rewrite theo từng thiết bị
RPU (garuda-dsp)
Ghi password MAIN_FORCE_NORMAL_MODE vào 2 holding register liên tiếp:
- Addr 48 (
MODBUS_FORCE_PASSWORD_LO) =0xFEED - Addr 49 (
MODBUS_FORCE_PASSWORD_HI) =0xCEE5
Firmware phát hiện password → gọi modbus_to_config() (bao gồm LimitModbusRange()) → write_all_flash() → soft_reset().
TPU (fenghuang-dsp)
Ghi giá trị 1 vào holding coil bit 49 (MODBUS_ALL_CFG_CMD_BIT) qua FC5.
Ở vòng lặp chính, update_modbus_input() phát hiện write_flash == 1 → gọi modbus_to_config() (bao gồm LimitModbusRange()) → write_all_flash() → soft_reset().
Lưu ý: Cả hai thiết bị đều gọi
DINTbên trongwrite_all_flash()trước khi ghi flash, khiến thiết bị không kịp gửi ACK cho lệnh trigger.NoResponseErrorlà bình thường và có nghĩa là rewrite đã được kích hoạt thành công. Script sẽ polling cho đến khi thiết bị boot xong trở lại (tối đaREWRITE_WAITgiây).
Danh sách register được test
Tất cả register nằm trong LimitModbusRange() đều được test, bao gồm:
uint16/int16: kẹp bởiLimitUpLoUint16()/LimitUpLoInt16()float32: kẹp bởiLimitUpLoFloat()— lưu dưới dạng 2 holding register liên tiếp thông quaEndianConvert32Bits(big-endian word order). Đọc bằng FC3, ghi bằng FC16.
Đối với TPU float32 register hiệu chỉnh (calibration), giới hạn được tính từ hằng số nominal trong global_variables.h:
- Gain (A):
[nominal × 0.9, nominal × 1.1] - Offset (B) nominal dương:
[nominal × 0.8, nominal × 1.2] - Offset (B) nominal âm: giới hạn tính theo cùng công thức nhưng giới hạn trên và dưới bị đảo ngược về mặt số học (firmware lưu theo cùng thứ tự bất kể dấu).
PFC_current_rms_cali_Bbị loại trừ vì nominal =0.0(giới hạn thu về[0, 0]).
So sánh giá trị float dùng dung sai tương đối FLOAT_EPSILON = 1e-4 (0.01%) để tính đến sai số làm tròn IEEE 754.
Nguồn giới hạn:
- RPU:
garuda-dsp/app/app_modbus/app_modbus.h,global_variables.h - TPU:
fenghuang-dsp/app/app_modbus/app_modbus.h,global_variables.h,sec_control.h,fan.h,app_inv.h
Quy trình test (batch)
- Phase 0 — Đọc và lưu tất cả giá trị default hiện tại.
- Phase 1 — Ghi tất cả giá trị dưới ngưỡng min vào register → trigger rewrite một lần duy nhất → đọc lại toàn bộ.
- PASS nếu giá trị đọc lại ==
min_val - FAIL nếu giá trị đọc lại !=
min_val
- PASS nếu giá trị đọc lại ==
- Phase 2 — Ghi tất cả giá trị vượt ngưỡng max vào register → trigger rewrite một lần duy nhất → đọc lại toàn bộ.
- PASS nếu giá trị đọc lại ==
max_val - FAIL nếu giá trị đọc lại !=
max_val
- PASS nếu giá trị đọc lại ==
- Restore — Ghi lại tất cả giá trị default → trigger rewrite để khôi phục flash về trạng thái ban đầu.
Dùng cách batch (ghi tất cả rồi trigger một lần) thay vì từng register một để giảm thời gian chờ reboot.
Báo cáo CSV
File được đặt tên <device>_modbus_limiter_report_<timestamp>.csv cùng thư mục với script.
| Cột | Nội dung |
|---|---|
| 1 | Tên register |
| 2 | Kiểu dữ liệu (uint16 / int16 / float32) |
| 3 | Giá trị default ban đầu |
| 4 | Giá trị min cho phép |
| 5 | Giá trị max cho phép |
| 6 | Giá trị dưới min được ghi vào |
| 7 | Giá trị đọc lại sau rewrite (below-min) |
| 8 | Kết quả: PASS / FAIL / ERR / N/A |
| 9 | Giá trị vượt max được ghi vào |
| 10 | Giá trị đọc lại sau rewrite (above-max) |
| 11 | Kết quả: PASS / FAIL / ERR / N/A |
N/A = không áp dụng (ví dụ: uint16 với min=0 không thể ghi giá trị âm, hoặc int16 với max=32767).
ERR = lỗi đọc/ghi Modbus.
Yêu cầu môi trường
- Python 3.10+
minimalmodbus(pip install minimalmodbus)- Kết nối RS-485 / USB-to-serial đến thiết bị (RPU hoặc TPU)
- Slave ID:
0x10(16), Baud: 9600, Mode: RTU