Files
2026-07-21 00:14:43 +08:00

9.2 KiB
Raw Permalink Blame History

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):

  • 0x0021TPU (fenghuang-dsp)
  • 0x0022RPU (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 DINT inside write_all_flash() before writing to flash, which means the device cannot send an ACK for the trigger write. NoResponseError on the trigger write is expected and indicates the rewrite was successfully initiated. The script polls until the device finishes booting (up to REWRITE_WAIT seconds).


Registers Under Test

All registers that appear inside LimitModbusRange() are tested, including:

  • uint16 / int16 registers clamped by LimitUpLoUint16() / LimitUpLoInt16()
  • float32 registers clamped by LimitUpLoFloat() — stored as two consecutive holding registers via EndianConvert32Bits (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_B is excluded because its nominal is 0.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)

  1. Phase 0 — Read and save all current default values.
  2. 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
  3. 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
  4. 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):

  • 0x0021TPU (fenghuang-dsp)
  • 0x0022RPU (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 DINT bên trong write_all_flash() trước khi ghi flash, khiến thiết bị không kịp gửi ACK cho lệnh trigger. NoResponseError là 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 đa REWRITE_WAIT giâ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ởi LimitUpLoUint16() / LimitUpLoInt16()
  • float32: kẹp bởi LimitUpLoFloat() — lưu dưới dạng 2 holding register liên tiếp thông qua EndianConvert32Bits (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_B bị 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)

  1. Phase 0 — Đọc và lưu tất cả giá trị default hiện tại.
  2. 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
  3. 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
  4. 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