Răsfoiți Sursa

存储访问协议调整为小端序,内部多字节类型根据同步命令回报的大端序还是小端序进行解析

avery 4 zile în urmă
părinte
comite
e49153b086

+ 8 - 4
domain/parameter-groups/model.js

@@ -343,11 +343,12 @@ function assignOptionalNumber(target, key, value) {
   target[key] = value
   target[key] = value
 }
 }
 
 
-function normalizeMemoryEndian(value) {
+function normalizeMemoryEndian(value, fallback = 'little') {
   const text = String(value || '').trim().toLowerCase()
   const text = String(value || '').trim().toLowerCase()
   if (text === 'little' || text === 'le' || text === '1') return 'little'
   if (text === 'little' || text === 'le' || text === '1') return 'little'
+  if (text === 'big' || text === 'be' || text === '0') return 'big'
 
 
-  return 'big'
+  return fallback
 }
 }
 
 
 function normalizeStorageCodeInfoCard(codeInfo = null) {
 function normalizeStorageCodeInfoCard(codeInfo = null) {
@@ -361,7 +362,7 @@ function normalizeStorageCodeInfoCard(codeInfo = null) {
     caveFreq: hasCodeInfo ? readOptionalNumber(codeInfo.caveFreq) : null,
     caveFreq: hasCodeInfo ? readOptionalNumber(codeInfo.caveFreq) : null,
     chipModel: hasCodeInfo ? String(codeInfo.chipModel || '').trim() : '',
     chipModel: hasCodeInfo ? String(codeInfo.chipModel || '').trim() : '',
     maxPacketLength: hasCodeInfo ? (Number(codeInfo.maxPacketLength) || 0) : 0,
     maxPacketLength: hasCodeInfo ? (Number(codeInfo.maxPacketLength) || 0) : 0,
-    memoryEndian: hasCodeInfo ? normalizeMemoryEndian(codeInfo.memoryEndian) : 'big',
+    memoryEndian: hasCodeInfo ? normalizeMemoryEndian(codeInfo.memoryEndian) : 'little',
     memoryEndianRaw: hasCodeInfo ? (Number(codeInfo.memoryEndianRaw) || 0) : 0,
     memoryEndianRaw: hasCodeInfo ? (Number(codeInfo.memoryEndianRaw) || 0) : 0,
     model: hasCodeInfo ? String(codeInfo.model || '').trim() : '',
     model: hasCodeInfo ? String(codeInfo.model || '').trim() : '',
     refVolt: hasCodeInfo
     refVolt: hasCodeInfo
@@ -379,6 +380,7 @@ function normalizeStorageCodeInfoCard(codeInfo = null) {
       codeInfoAddressWidth: card.addressWidth,
       codeInfoAddressWidth: card.addressWidth,
       maxPacketLength: card.maxPacketLength,
       maxPacketLength: card.maxPacketLength,
       memoryEndian: card.memoryEndian,
       memoryEndian: card.memoryEndian,
+      memoryEndianRaw: card.memoryEndianRaw,
       storageAddressWidth: card.addressWidth
       storageAddressWidth: card.addressWidth
     }
     }
     : {}
     : {}
@@ -477,7 +479,9 @@ function normalizeRegister(register, group, index, address, byteOffset = 0) {
   const savedValue = getRegisterSavedValue(register)
   const savedValue = getRegisterSavedValue(register)
   const inputValue = savedValue === null ? defaultValue : savedValue
   const inputValue = savedValue === null ? defaultValue : savedValue
   const rawValue = register.rawValue === undefined ? null : register.rawValue
   const rawValue = register.rawValue === undefined ? null : register.rawValue
-  const memoryEndian = isStorageMemory ? normalizeMemoryEndian(register.memoryEndian || group.codeInfoContext && group.codeInfoContext.memoryEndian) : 'big'
+  const memoryEndian = isStorageMemory
+    ? normalizeMemoryEndian(register.memoryEndian || group.codeInfoContext && group.codeInfoContext.memoryEndian)
+    : 'big'
   const storageAddressWidth = isStorageMemory
   const storageAddressWidth = isStorageMemory
     ? resolveStorageAddressWidth({
     ? resolveStorageAddressWidth({
       ...group,
       ...group,

+ 4 - 7
domain/parameter-groups/value-codec.js

@@ -3,6 +3,7 @@ const {
 } = require('../../utils/base-utils.js')
 } = require('../../utils/base-utils.js')
 const {
 const {
   bytesToWords,
   bytesToWords,
+  formatHexNumber,
   wordsToBytes
   wordsToBytes
 } = require('../../utils/binary-utils.js')
 } = require('../../utils/binary-utils.js')
 const {
 const {
@@ -47,20 +48,16 @@ const {
   validateNumericValue
   validateNumericValue
 } = require('./value-number.js')
 } = require('./value-number.js')
 
 
-function padWordHex(value) {
-  return Number(value || 0).toString(16).toUpperCase().padStart(4, '0')
-}
-
 function formatRawWordText(words = []) {
 function formatRawWordText(words = []) {
   if (!Array.isArray(words) || !words.length) return '--'
   if (!Array.isArray(words) || !words.length) return '--'
 
 
-  return words.map((word) => `0x${padWordHex(word)}`).join(' ')
+  return words.map((word) => `0x${formatHexNumber(word, 4)}`).join(' ')
 }
 }
 
 
 function formatRawByteText(bytes = []) {
 function formatRawByteText(bytes = []) {
   if (!Array.isArray(bytes) || !bytes.length) return '--'
   if (!Array.isArray(bytes) || !bytes.length) return '--'
 
 
-  return bytes.map((byte) => `0x${(Number(byte) & 0xFF).toString(16).toUpperCase().padStart(2, '0')}`).join(' ')
+  return bytes.map((byte) => `0x${formatHexNumber(Number(byte) & 0xFF, 2)}`).join(' ')
 }
 }
 
 
 function formatRawByteTextWithDefault(bytes = [], byteLength = 1) {
 function formatRawByteTextWithDefault(bytes = [], byteLength = 1) {
@@ -68,7 +65,7 @@ function formatRawByteTextWithDefault(bytes = [], byteLength = 1) {
   const source = Array.isArray(bytes) ? bytes : []
   const source = Array.isArray(bytes) ? bytes : []
 
 
   return Array.from({ length: safeLength }, (_, index) => (
   return Array.from({ length: safeLength }, (_, index) => (
-    `0x${(Number(source[index] || 0) & 0xFF).toString(16).toUpperCase().padStart(2, '0')}`
+    `0x${formatHexNumber(Number(source[index] || 0) & 0xFF, 2)}`
   )).join(' ')
   )).join(' ')
 }
 }
 
 

+ 37 - 32
domain/storage-access/code-info-parser.js

@@ -1,6 +1,10 @@
 const {
 const {
   bytesToHex,
   bytesToHex,
   bytesToUtf8Text,
   bytesToUtf8Text,
+  formatHexNumber,
+  readUint16BE,
+  readUint16LE,
+  readUint32LE,
   trimTrailingNullBytes
   trimTrailingNullBytes
 } = require('../../utils/binary-utils.js')
 } = require('../../utils/binary-utils.js')
 
 
@@ -102,30 +106,30 @@ function toBytes(bytes) {
   return Array.prototype.slice.call(bytes || []).map((byte) => Number(byte) & 0xFF)
   return Array.prototype.slice.call(bytes || []).map((byte) => Number(byte) & 0xFF)
 }
 }
 
 
-function readUint16(bytes, offset) {
-  if (offset + 1 >= bytes.length) return 0
+function normalizeMemoryEndian(value, fallback = MEMORY_ENDIAN.LITTLE) {
+  const text = String(value || '').trim().toLowerCase()
+  if (text === 'little' || text === 'le' || text === '1') return MEMORY_ENDIAN.LITTLE
+  if (text === 'big' || text === 'be' || text === '0') return MEMORY_ENDIAN.BIG
 
 
-  return (((bytes[offset] || 0) << 8) | (bytes[offset + 1] || 0)) & 0xFFFF
+  return fallback
 }
 }
 
 
-function readUint32(bytes, offset) {
-  if (offset + 3 >= bytes.length) return 0
+function applyMemoryEndian(bytes = [], memoryEndian = MEMORY_ENDIAN.LITTLE) {
+  const source = Array.isArray(bytes) ? bytes.slice() : []
 
 
-  return (
-    ((bytes[offset] || 0) * 0x1000000)
-    + (((bytes[offset + 1] || 0) << 16) >>> 0)
-    + (((bytes[offset + 2] || 0) << 8) >>> 0)
-    + (bytes[offset + 3] || 0)
-  ) >>> 0
+  return normalizeMemoryEndian(memoryEndian) === MEMORY_ENDIAN.LITTLE
+    ? source.reverse()
+    : source
 }
 }
 
 
-function readFloat(bytes, offset) {
+function readFloat(bytes, offset, memoryEndian = MEMORY_ENDIAN.LITTLE) {
   if (offset + 3 >= bytes.length) return 0
   if (offset + 3 >= bytes.length) return 0
 
 
   const buffer = new ArrayBuffer(4)
   const buffer = new ArrayBuffer(4)
   const view = new DataView(buffer)
   const view = new DataView(buffer)
+  const source = applyMemoryEndian(bytes.slice(offset, offset + 4), memoryEndian)
   for (let index = 0; index < 4; index += 1) {
   for (let index = 0; index < 4; index += 1) {
-    view.setUint8(index, bytes[offset + index] || 0)
+    view.setUint8(index, source[index] || 0)
   }
   }
 
 
   return view.getFloat32(0, false)
   return view.getFloat32(0, false)
@@ -169,7 +173,7 @@ function formatAddress(address, addressWidth = 32) {
   const numberValue = Math.max(0, Math.floor(Number(address) || 0))
   const numberValue = Math.max(0, Math.floor(Number(address) || 0))
   const hexWidth = normalizeCodeInfoAddressWidth(addressWidth) === 16 ? 4 : 8
   const hexWidth = normalizeCodeInfoAddressWidth(addressWidth) === 16 ? 4 : 8
 
 
-  return `0x${numberValue.toString(16).toUpperCase().padStart(hexWidth, '0')}`
+  return `0x${formatHexNumber(numberValue, hexWidth)}`
 }
 }
 
 
 function normalizeTypeName(value, fallback) {
 function normalizeTypeName(value, fallback) {
@@ -182,13 +186,6 @@ function getEntryKindText(entryKind) {
   return CODE_INFO_ENTRY_KIND_TEXT[entryKind] || 'unknown'
   return CODE_INFO_ENTRY_KIND_TEXT[entryKind] || 'unknown'
 }
 }
 
 
-function normalizeMemoryEndian(value) {
-  const text = String(value || '').trim().toLowerCase()
-  if (text === 'little' || text === 'le' || text === '1') return MEMORY_ENDIAN.LITTLE
-
-  return MEMORY_ENDIAN.BIG
-}
-
 function resolveCodeInfoByteLength(sourceLength, options = {}) {
 function resolveCodeInfoByteLength(sourceLength, options = {}) {
   const expectedLength = Number(
   const expectedLength = Number(
     options.codeInfoByteLength === undefined ? options.byteLength : options.codeInfoByteLength
     options.codeInfoByteLength === undefined ? options.byteLength : options.codeInfoByteLength
@@ -218,9 +215,9 @@ function parseMemoryEntry(valueBytes, index, type) {
   const memType = 0
   const memType = 0
   const addressOffset = 0
   const addressOffset = 0
   const byteAddr = layout.addressByteLength === 2
   const byteAddr = layout.addressByteLength === 2
-    ? readUint16(valueBytes, addressOffset)
-    : readUint32(valueBytes, addressOffset)
-  const byteLength = readUint16(valueBytes, addressOffset + layout.addressByteLength)
+    ? readUint16LE(valueBytes, addressOffset)
+    : readUint32LE(valueBytes, addressOffset)
+  const byteLength = readUint16LE(valueBytes, addressOffset + layout.addressByteLength)
   const nameOffset = layout.nameOffset
   const nameOffset = layout.nameOffset
   const entryKind = layout.entryKind
   const entryKind = layout.entryKind
   const entryKindText = getEntryKindText(entryKind)
   const entryKindText = getEntryKindText(entryKind)
@@ -250,7 +247,13 @@ function parseMemoryEntry(valueBytes, index, type) {
   }
   }
 }
 }
 
 
-function applyCodeInfoTlvValue(target, type, valueBytes) {
+function readMemoryEndianUint16(bytes, memoryEndian) {
+  return readUint16BE(applyMemoryEndian(bytes.slice(0, 2), memoryEndian), 0)
+}
+
+function applyCodeInfoTlvValue(target, type, valueBytes, options = {}) {
+  const memoryEndian = normalizeMemoryEndian(options.memoryEndian)
+
   switch (type) {
   switch (type) {
     case CODE_INFO_TLV.CAVE_FREQ:
     case CODE_INFO_TLV.CAVE_FREQ:
       if (valueBytes.length < 1) throw new Error('CodeInfo cave_freq TLV 长度无效')
       if (valueBytes.length < 1) throw new Error('CodeInfo cave_freq TLV 长度无效')
@@ -267,15 +270,15 @@ function applyCodeInfoTlvValue(target, type, valueBytes) {
       break
       break
     case CODE_INFO_TLV.RS_SHUNT:
     case CODE_INFO_TLV.RS_SHUNT:
       if (valueBytes.length < 2) throw new Error('CodeInfo rs_shunt TLV 长度无效')
       if (valueBytes.length < 2) throw new Error('CodeInfo rs_shunt TLV 长度无效')
-      target.rsShunt = readUint16(valueBytes, 0)
+      target.rsShunt = readMemoryEndianUint16(valueBytes, memoryEndian)
       break
       break
     case CODE_INFO_TLV.BUS_DIV:
     case CODE_INFO_TLV.BUS_DIV:
       if (valueBytes.length < 4) throw new Error('CodeInfo bus_div TLV 长度无效')
       if (valueBytes.length < 4) throw new Error('CodeInfo bus_div TLV 长度无效')
-      target.busDiv = readFloat(valueBytes, 0)
+      target.busDiv = readFloat(valueBytes, 0, memoryEndian)
       break
       break
     case CODE_INFO_TLV.ALONG_DIV:
     case CODE_INFO_TLV.ALONG_DIV:
       if (valueBytes.length < 4) throw new Error('CodeInfo along_div TLV 长度无效')
       if (valueBytes.length < 4) throw new Error('CodeInfo along_div TLV 长度无效')
-      target.alongDiv = readFloat(valueBytes, 0)
+      target.alongDiv = readFloat(valueBytes, 0, memoryEndian)
       break
       break
     case CODE_INFO_TLV.CHIP_MODEL:
     case CODE_INFO_TLV.CHIP_MODEL:
       target.chipModel = readTlvText(valueBytes)
       target.chipModel = readTlvText(valueBytes)
@@ -291,7 +294,7 @@ function applyCodeInfoTlvValue(target, type, valueBytes) {
   }
   }
 }
 }
 
 
-function parseCodeInfoTlvs(bytes) {
+function parseCodeInfoTlvs(bytes, options = {}) {
   const info = {
   const info = {
     entries: [],
     entries: [],
     tlvItems: []
     tlvItems: []
@@ -323,7 +326,7 @@ function parseCodeInfoTlvs(bytes) {
     if (entry) {
     if (entry) {
       info.entries.push(entry)
       info.entries.push(entry)
     } else {
     } else {
-      applyCodeInfoTlvValue(info, type, valueBytes)
+      applyCodeInfoTlvValue(info, type, valueBytes, options)
     }
     }
     offset = nextOffset
     offset = nextOffset
   }
   }
@@ -338,7 +341,8 @@ function parseCodeInfo(bytes, options = {}) {
 
 
   const addressWidth = normalizeCodeInfoAddressWidth(options.addressWidth)
   const addressWidth = normalizeCodeInfoAddressWidth(options.addressWidth)
   const maxPacketLength = Number(options.maxPacketLength || 0) & 0xFFFF
   const maxPacketLength = Number(options.maxPacketLength || 0) & 0xFFFF
-  const tlvInfo = parseCodeInfoTlvs(source)
+  const memoryEndian = normalizeMemoryEndian(options.memoryEndian)
+  const tlvInfo = parseCodeInfoTlvs(source, { memoryEndian })
   const entries = tlvInfo.entries
   const entries = tlvInfo.entries
   const entryCount = entries.length
   const entryCount = entries.length
   const entryAddressByteLengths = entries
   const entryAddressByteLengths = entries
@@ -352,7 +356,7 @@ function parseCodeInfo(bytes, options = {}) {
     byteLength: codeInfoByteLength,
     byteLength: codeInfoByteLength,
     entryCount,
     entryCount,
     maxPacketLength,
     maxPacketLength,
-    memoryEndian: normalizeMemoryEndian(options.memoryEndian),
+    memoryEndian,
     memoryEndianRaw: Number(options.memoryEndianRaw || options.memoryEndianMark || 0) & 0xFFFF,
     memoryEndianRaw: Number(options.memoryEndianRaw || options.memoryEndianMark || 0) & 0xFFFF,
     rawHex: bytesToHex(source, ' '),
     rawHex: bytesToHex(source, ' '),
     structEntryAddressByteLength: entryAddressByteLengths.length === 1 ? entryAddressByteLengths[0] : 0,
     structEntryAddressByteLength: entryAddressByteLengths.length === 1 ? entryAddressByteLengths[0] : 0,
@@ -416,6 +420,7 @@ function createCodeInfoContext(codeInfo = {}) {
     codeInfoAddressWidth: codeInfo.addressWidth,
     codeInfoAddressWidth: codeInfo.addressWidth,
     maxPacketLength: codeInfo.maxPacketLength,
     maxPacketLength: codeInfo.maxPacketLength,
     memoryEndian: codeInfo.memoryEndian,
     memoryEndian: codeInfo.memoryEndian,
+    memoryEndianRaw: Number(codeInfo.memoryEndianRaw || 0) & 0xFFFF,
     storageAddressWidth: codeInfo.addressWidth
     storageAddressWidth: codeInfo.addressWidth
   }
   }
 }
 }

+ 5 - 13
features/parameter-groups/io.js

@@ -6,8 +6,11 @@ const {
 } = require('../../utils/binary-utils.js')
 } = require('../../utils/binary-utils.js')
 const transport = require('../../transport/ble-core.js')
 const transport = require('../../transport/ble-core.js')
 const modbusClient = require('../modbus-rtu/service.js')
 const modbusClient = require('../modbus-rtu/service.js')
-const storageAccessProtocol = require('../../protocols/storage-access/index.js')
 const storageAccessProtocolIo = require('../storage-access/protocol-io.js')
 const storageAccessProtocolIo = require('../storage-access/protocol-io.js')
+const {
+  getMemoryTypeFromName,
+  isReadOnlyMemoryType
+} = require('../storage-access/memory-areas.js')
 const {
 const {
   decodeRegisterFromByteCache,
   decodeRegisterFromByteCache,
   decodeRegisterFromWordCache,
   decodeRegisterFromWordCache,
@@ -31,22 +34,11 @@ const {
   splitWordSpans
   splitWordSpans
 } = require('../../domain/parameter-groups/model.js')
 } = require('../../domain/parameter-groups/model.js')
 
 
-const STORAGE_ACCESS_READ_ONLY_AREAS = [storageAccessProtocol.AREA.CODEINFO, storageAccessProtocol.AREA.CODE]
 const MAX_STORAGE_ACCESS_ADDRESS = 0xFFFFFFFF
 const MAX_STORAGE_ACCESS_ADDRESS = 0xFFFFFFFF
 const MAX_STORAGE_ACCESS_BYTE_LENGTH = 0xFFFFFFFF
 const MAX_STORAGE_ACCESS_BYTE_LENGTH = 0xFFFFFFFF
 
 
 function getMemoryType(group = {}) {
 function getMemoryType(group = {}) {
-  const memoryArea = String(group.sourceMemoryArea || '').trim().toUpperCase()
-  if (memoryArea === 'ADDR32' || memoryArea === 'ADDRESS32') return storageAccessProtocol.AREA.ADDR32
-  if (memoryArea === 'BIT') return storageAccessProtocol.AREA.DATA
-
-  const area = storageAccessProtocol.AREA[memoryArea]
-
-  return area === undefined ? null : area
-}
-
-function isReadOnlyMemoryType(memoryType) {
-  return STORAGE_ACCESS_READ_ONLY_AREAS.indexOf(memoryType) >= 0
+  return getMemoryTypeFromName(group.sourceMemoryArea)
 }
 }
 
 
 function isMemoryGroup(group = {}) {
 function isMemoryGroup(group = {}) {

+ 2 - 2
features/settings/protocol-implementation.js

@@ -14,8 +14,8 @@ const GUIDE = {
     { id: 'frame', text: 'CMD bit7 为故障位,仅回帧出现;bit6 为特殊指令位;普通读写中 bit3 为读写位,bit4/bit5 保留,bit0~bit2 表示区域。' },
     { id: 'frame', text: 'CMD bit7 为故障位,仅回帧出现;bit6 为特殊指令位;普通读写中 bit3 为读写位,bit4/bit5 保留,bit0~bit2 表示区域。' },
     { id: 'addr', text: '普通区域 0x00 为 codeinfo 只读描述符;0x01/0x02/0x03/0x04 为 DATA、IDATA、XDATA、CODE 的 16 位地址;0x07 为 ADDR32 的 32 位地址;0x05~0x06 保留。' },
     { id: 'addr', text: '普通区域 0x00 为 codeinfo 只读描述符;0x01/0x02/0x03/0x04 为 DATA、IDATA、XDATA、CODE 的 16 位地址;0x07 为 ADDR32 的 32 位地址;0x05~0x06 保留。' },
     { id: 'info', text: 'bit6=1 时 bit0~bit5 表示特殊指令码;当前仅定义 0x01 复位,因此复位帧 CMD=0x41。' },
     { id: 'info', text: 'bit6=1 时 bit0~bit5 表示特殊指令码;当前仅定义 0x01 复位,因此复位帧 CMD=0x41。' },
-    { id: 'control', text: 'CodeInfo 同步先发送 00+CRC 读取 area=0x00 描述符,数据区返回 TLV 起始 addr32、len16、地址位宽和最大包长。' },
-    { id: 'struct', text: 'CodeInfo 使用 TYPE + LEN8 + VALUE 的纯 TLV 信息块;0x01~0x08 为固定内存入口,VALUE 为 addr(2/4)+len16+name_len8+name;单独变量由 TLV 给长度,UI 或 enum 导入配置解释类型。' },
+    { id: 'control', text: 'CodeInfo 同步先发送 00+CRC 读取 area=0x00 描述符,数据区返回小端序 TLV 起始 addr32、len16、地址位宽、最大包长和原始 ENDIAN_MARK(55AA/AA55)。' },
+    { id: 'struct', text: 'CodeInfo 使用 TYPE + LEN8 + VALUE 的纯 TLV 信息块;0x01~0x08 为固定内存入口,VALUE 为小端序 addr(2/4)+len16+name_len8+name;单独变量由 TLV 给长度,UI 或 enum 导入配置解释类型。' },
     { id: 'bootloader', text: 'Bootloader 升级前先发送存储访问复位帧 CMD=0x41,再在 500ms 内每 50ms 发送一次 Bootloader 握手帧。' },
     { id: 'bootloader', text: 'Bootloader 升级前先发送存储访问复位帧 CMD=0x41,再在 500ms 内每 50ms 发送一次 Bootloader 握手帧。' },
     { id: 'source', text: '协议实现源码已暂时移除,设置页仅保留文件占位与说明,避免给出过期从机实现。' }
     { id: 'source', text: '协议实现源码已暂时移除,设置页仅保留文件占位与说明,避免给出过期从机实现。' }
   ]
   ]

+ 5 - 8
features/storage-access/code-info-sync.js

@@ -1,5 +1,8 @@
 const storageAccessProtocol = require('../../protocols/storage-access/index.js')
 const storageAccessProtocol = require('../../protocols/storage-access/index.js')
 const transport = require('../../transport/ble-core.js')
 const transport = require('../../transport/ble-core.js')
+const {
+  formatHexNumber
+} = require('../../utils/binary-utils.js')
 const {
 const {
   createGroupsFromCodeInfo,
   createGroupsFromCodeInfo,
   parseCodeInfo
   parseCodeInfo
@@ -10,12 +13,6 @@ const {
 } = require('../../domain/parameter-groups/model.js')
 } = require('../../domain/parameter-groups/model.js')
 const protocolIo = require('./protocol-io.js')
 const protocolIo = require('./protocol-io.js')
 
 
-function formatDwordHex(value) {
-  const numberValue = Math.max(0, Math.floor(Number(value) || 0))
-
-  return numberValue.toString(16).toUpperCase().padStart(8, '0')
-}
-
 function showCodeInfoError(label, message, options = {}) {
 function showCodeInfoError(label, message, options = {}) {
   if (options.showModal === false) return
   if (options.showModal === false) return
 
 
@@ -150,10 +147,10 @@ async function syncCodeInfo(options = {}) {
 
 
   return {
   return {
     codeInfoAddress: result.codeInfoAddress,
     codeInfoAddress: result.codeInfoAddress,
-    codeInfoAddressText: formatDwordHex(result.codeInfoAddress),
+    codeInfoAddressText: formatHexNumber(result.codeInfoAddress, 8),
     codeInfoAddressWidth: result.codeInfoAddressWidth,
     codeInfoAddressWidth: result.codeInfoAddressWidth,
     codeInfoByteLength: result.codeInfoByteLength,
     codeInfoByteLength: result.codeInfoByteLength,
-    codeInfoByteLengthText: formatDwordHex(result.codeInfoByteLength),
+    codeInfoByteLengthText: formatHexNumber(result.codeInfoByteLength, 8),
     codeInfoBytes: result.codeInfoBytes,
     codeInfoBytes: result.codeInfoBytes,
     codeInfo,
     codeInfo,
     codeInfoDescriptorBytes: result.codeInfoDescriptorBytes,
     codeInfoDescriptorBytes: result.codeInfoDescriptorBytes,

+ 7 - 17
features/storage-access/manual-command.js

@@ -1,6 +1,7 @@
 const storageAccessProtocol = require('../../protocols/storage-access/index.js')
 const storageAccessProtocol = require('../../protocols/storage-access/index.js')
 const {
 const {
-  bytesToHex
+  bytesToHex,
+  formatHexNumber
 } = require('../../utils/binary-utils.js')
 } = require('../../utils/binary-utils.js')
 const {
 const {
   normalizeHexDwordText,
   normalizeHexDwordText,
@@ -13,6 +14,9 @@ const {
   validateHexText,
   validateHexText,
   validateHexWordText
   validateHexWordText
 } = require('../../utils/validation.js')
 } = require('../../utils/validation.js')
+const {
+  getManualMemoryAreaOptions
+} = require('./memory-areas.js')
 const protocolIo = require('./protocol-io.js')
 const protocolIo = require('./protocol-io.js')
 
 
 const MEMORY_COMMANDS = [
 const MEMORY_COMMANDS = [
@@ -20,26 +24,12 @@ const MEMORY_COMMANDS = [
   { key: 'write', label: '写入', description: '按字节写入内存' }
   { key: 'write', label: '写入', description: '按字节写入内存' }
 ]
 ]
 
 
-const MEMORY_AREAS = [
-  { key: storageAccessProtocol.AREA.ADDR32, label: 'addr32', name: 'ADDR32', addressWidth: 32 },
-  { key: storageAccessProtocol.AREA.DATA, label: 'data', name: 'DATA' },
-  { key: storageAccessProtocol.AREA.IDATA, label: 'idata', name: 'IDATA' },
-  { key: storageAccessProtocol.AREA.XDATA, label: 'xdata', name: 'XDATA' },
-  { key: storageAccessProtocol.AREA.CODE, label: 'code', name: 'CODE', readOnly: true }
-]
-
 const CONTROL_COMMANDS = [
 const CONTROL_COMMANDS = [
   { key: 'reset', label: '复位', op: storageAccessProtocol.CONTROL_OP.RESET }
   { key: 'reset', label: '复位', op: storageAccessProtocol.CONTROL_OP.RESET }
 ]
 ]
 
 
-function formatDwordHex(value) {
-  const numberValue = Math.max(0, Math.floor(Number(value) || 0))
-
-  return numberValue.toString(16).toUpperCase().padStart(8, '0')
-}
-
 function getMemoryAreaOptions() {
 function getMemoryAreaOptions() {
-  return MEMORY_AREAS.map((area) => ({ ...area }))
+  return getManualMemoryAreaOptions()
 }
 }
 
 
 function resolveMemoryCommand(index) {
 function resolveMemoryCommand(index) {
@@ -153,7 +143,7 @@ function normalizeMemoryCommandState(current = {}, changed = {}) {
       ? buildMemoryPreview(command.key, area.key, address, byteLength, dataBytes)
       ? buildMemoryPreview(command.key, area.key, address, byteLength, dataBytes)
       : '',
       : '',
     storageAccessPreviewText: canPreview
     storageAccessPreviewText: canPreview
-      ? `${area.label} 0x${addressWidth === 32 ? formatDwordHex(address) : address.toString(16).toUpperCase().padStart(4, '0')} / ${byteLength} bytes`
+      ? `${area.label} 0x${formatHexNumber(address, addressWidth === 32 ? 8 : 4)} / ${byteLength} bytes`
       : '',
       : '',
     storageAccessShowWriteData: command.key === 'write',
     storageAccessShowWriteData: command.key === 'write',
     storageAccessTitleText: `${command.label}命令`
     storageAccessTitleText: `${command.label}命令`

+ 39 - 0
features/storage-access/memory-areas.js

@@ -0,0 +1,39 @@
+const storageAccessProtocol = require('../../protocols/storage-access/index.js')
+
+const READ_ONLY_MEMORY_AREAS = [
+  storageAccessProtocol.AREA.CODEINFO,
+  storageAccessProtocol.AREA.CODE
+]
+
+const MANUAL_MEMORY_AREAS = [
+  { key: storageAccessProtocol.AREA.ADDR32, label: 'addr32', name: 'ADDR32', addressWidth: 32 },
+  { key: storageAccessProtocol.AREA.DATA, label: 'data', name: 'DATA' },
+  { key: storageAccessProtocol.AREA.IDATA, label: 'idata', name: 'IDATA' },
+  { key: storageAccessProtocol.AREA.XDATA, label: 'xdata', name: 'XDATA' },
+  { key: storageAccessProtocol.AREA.CODE, label: 'code', name: 'CODE', readOnly: true }
+]
+
+function getManualMemoryAreaOptions() {
+  return MANUAL_MEMORY_AREAS.map((area) => ({ ...area }))
+}
+
+function getMemoryTypeFromName(value) {
+  const memoryArea = String(value || '').trim().toUpperCase()
+  if (memoryArea === 'ADDR32' || memoryArea === 'ADDRESS32') return storageAccessProtocol.AREA.ADDR32
+  if (memoryArea === 'BIT') return storageAccessProtocol.AREA.DATA
+
+  const area = storageAccessProtocol.AREA[memoryArea]
+
+  return area === undefined ? null : area
+}
+
+function isReadOnlyMemoryType(memoryType) {
+  return READ_ONLY_MEMORY_AREAS.indexOf(Number(memoryType)) >= 0
+}
+
+module.exports = {
+  getManualMemoryAreaOptions,
+  getMemoryTypeFromName,
+  isReadOnlyMemoryType,
+  READ_ONLY_MEMORY_AREAS
+}

+ 30 - 21
features/storage-access/protocol-io.js

@@ -61,6 +61,28 @@ function getSyncedDeviceCaps() {
   }
   }
 }
 }
 
 
+function createTransportOptions(protocolIoOptions = {}) {
+  return {
+    maxFrameBytes: protocolIoOptions.maxFrameBytes,
+    showModal: protocolIoOptions.showModal
+  }
+}
+
+function notifyChunk(protocolIoOptions = {}, response, chunk) {
+  if (typeof protocolIoOptions.onChunk === 'function') {
+    protocolIoOptions.onChunk(response, chunk)
+  }
+}
+
+function sendStorageFrame(frameBytes, label, expected, protocolIoOptions = {}) {
+  return transport.sendManagedFrame(
+    frameBytes,
+    label,
+    expected,
+    createTransportOptions(protocolIoOptions)
+  )
+}
+
 async function readMemory(area, startAddress, byteLength, label, kind, options = {}) {
 async function readMemory(area, startAddress, byteLength, label, kind, options = {}) {
   const protocolIoOptions = normalizeProtocolIoOptions(options)
   const protocolIoOptions = normalizeProtocolIoOptions(options)
   const normalizedArea = storageAccessProtocol.normalizeMemoryArea(area)
   const normalizedArea = storageAccessProtocol.normalizeMemoryArea(area)
@@ -71,16 +93,13 @@ async function readMemory(area, startAddress, byteLength, label, kind, options =
   })
   })
 
 
   for (const chunk of chunks) {
   for (const chunk of chunks) {
-    const response = await transport.sendManagedFrame(
+    const response = await sendStorageFrame(
       storageAccessProtocol.buildReadFrame(normalizedArea, chunk.address, chunk.quantity, {
       storageAccessProtocol.buildReadFrame(normalizedArea, chunk.address, chunk.quantity, {
         maxFrameBytes: protocolIoOptions.maxFrameBytes
         maxFrameBytes: protocolIoOptions.maxFrameBytes
       }),
       }),
       storageAccessProtocol.getChunkLabel(label, chunks, chunk),
       storageAccessProtocol.getChunkLabel(label, chunks, chunk),
       storageAccessProtocol.createExpected(normalizedArea, chunk.address, chunk.quantity, false, kind),
       storageAccessProtocol.createExpected(normalizedArea, chunk.address, chunk.quantity, false, kind),
-      {
-        maxFrameBytes: protocolIoOptions.maxFrameBytes,
-        showModal: protocolIoOptions.showModal
-      }
+      protocolIoOptions
     )
     )
     if (!response) return null
     if (!response) return null
 
 
@@ -89,9 +108,7 @@ async function readMemory(area, startAddress, byteLength, label, kind, options =
       bytes[chunk.address - startAddress + index] = Number(byte) & 0xFF
       bytes[chunk.address - startAddress + index] = Number(byte) & 0xFF
     })
     })
 
 
-    if (typeof protocolIoOptions.onChunk === 'function') {
-      protocolIoOptions.onChunk(response, chunk)
-    }
+    notifyChunk(protocolIoOptions, response, chunk)
   }
   }
 
 
   return bytes
   return bytes
@@ -106,22 +123,17 @@ async function writeMemory(area, startAddress, bytes, label, kind, options = {})
   })
   })
 
 
   for (const chunk of chunks) {
   for (const chunk of chunks) {
-    const response = await transport.sendManagedFrame(
+    const response = await sendStorageFrame(
       storageAccessProtocol.buildWriteFrame(normalizedArea, chunk.address, chunk.dataBytes, {
       storageAccessProtocol.buildWriteFrame(normalizedArea, chunk.address, chunk.dataBytes, {
         maxFrameBytes: protocolIoOptions.maxFrameBytes
         maxFrameBytes: protocolIoOptions.maxFrameBytes
       }),
       }),
       storageAccessProtocol.getChunkLabel(label, chunks, chunk),
       storageAccessProtocol.getChunkLabel(label, chunks, chunk),
       storageAccessProtocol.createExpected(normalizedArea, chunk.address, chunk.quantity, true, kind),
       storageAccessProtocol.createExpected(normalizedArea, chunk.address, chunk.quantity, true, kind),
-      {
-        maxFrameBytes: protocolIoOptions.maxFrameBytes,
-        showModal: protocolIoOptions.showModal
-      }
+      protocolIoOptions
     )
     )
     if (!response) return false
     if (!response) return false
 
 
-    if (typeof protocolIoOptions.onChunk === 'function') {
-      protocolIoOptions.onChunk(response, chunk)
-    }
+    notifyChunk(protocolIoOptions, response, chunk)
   }
   }
 
 
   return true
   return true
@@ -129,16 +141,13 @@ async function writeMemory(area, startAddress, bytes, label, kind, options = {})
 
 
 async function executeControl(operation, dataBytes, label, kind, options = {}) {
 async function executeControl(operation, dataBytes, label, kind, options = {}) {
   const protocolIoOptions = normalizeProtocolIoOptions(options)
   const protocolIoOptions = normalizeProtocolIoOptions(options)
-  const response = await transport.sendManagedFrame(
+  const response = await sendStorageFrame(
     storageAccessProtocol.buildControlFrame(operation, dataBytes),
     storageAccessProtocol.buildControlFrame(operation, dataBytes),
     label,
     label,
     storageAccessProtocol.createControlExpected(operation, kind, {
     storageAccessProtocol.createControlExpected(operation, kind, {
       expectedByteLength: protocolIoOptions.expectedByteLength
       expectedByteLength: protocolIoOptions.expectedByteLength
     }),
     }),
-    {
-      maxFrameBytes: protocolIoOptions.maxFrameBytes,
-      showModal: protocolIoOptions.showModal
-    }
+    protocolIoOptions
   )
   )
 
 
   return response || null
   return response || null

+ 59 - 47
features/tools/index.js

@@ -17,16 +17,22 @@ const refrigerationHandlers = require('./handlers/refrigeration.js')
 const smdCodeHandlers = require('./handlers/smd-code.js')
 const smdCodeHandlers = require('./handlers/smd-code.js')
 const threePhasePowerHandlers = require('./handlers/three-phase-power.js')
 const threePhasePowerHandlers = require('./handlers/three-phase-power.js')
 
 
-const TOOL_ENTRIES = [
+const TOOL_MODULES = [
   { view: 'bootloader', label: 'BootLoader升级', icon: 'icon-chip', iconSrc: '/assets/icons/chip-white.png' },
   { view: 'bootloader', label: 'BootLoader升级', icon: 'icon-chip', iconSrc: '/assets/icons/chip-white.png' },
-  { view: 'crc', label: 'CRC与哈希计算', icon: 'icon-crc', iconSrc: '/assets/icons/hash-white.png' },
-  { view: 'asciiCode', label: 'ASCII/数值转换', icon: 'icon-terminal', iconSrc: '/assets/icons/terminal-white.png' },
-  { view: 'filter', label: '滤波器计算', icon: 'icon-filter', iconSrc: '/assets/icons/funnel-white.png' },
-  { view: 'reactance', label: '电抗计算', icon: 'icon-reactance', iconSrc: '/assets/icons/audio-waveform-white.png' },
-  { view: 'smdCode', label: '贴片电阻/容代码', icon: 'icon-smd', iconSrc: '/assets/icons/microchip-white.png' },
-  { view: 'refrigeration', label: '制冷计算', icon: 'icon-snow', iconSrc: '/assets/icons/snowflake-white.png' },
-  { view: 'threePhasePower', label: '三相功率计算', icon: 'icon-three-phase', iconSrc: '/assets/icons/zap-white.png' }
+  { view: 'crc', label: 'CRC与哈希计算', icon: 'icon-crc', iconSrc: '/assets/icons/hash-white.png', calculator: crcTool, handlers: crcHandlers.handlers },
+  { view: 'asciiCode', label: 'ASCII/数值转换', icon: 'icon-terminal', iconSrc: '/assets/icons/terminal-white.png', calculator: asciiCodeTool, handlers: asciiCodeHandlers.handlers },
+  { view: 'filter', label: '滤波器计算', icon: 'icon-filter', iconSrc: '/assets/icons/funnel-white.png', calculator: filterCalculator, handlers: filterHandlers.handlers },
+  { view: 'reactance', label: '电抗计算', icon: 'icon-reactance', iconSrc: '/assets/icons/audio-waveform-white.png', calculator: reactanceCalculator, handlers: reactanceHandlers.handlers },
+  { view: 'smdCode', label: '贴片电阻/容代码', icon: 'icon-smd', iconSrc: '/assets/icons/microchip-white.png', calculator: smdCodeCalculator, handlers: smdCodeHandlers.handlers },
+  { view: 'refrigeration', label: '制冷计算', icon: 'icon-snow', iconSrc: '/assets/icons/snowflake-white.png', calculator: refrigerationCalculator, handlers: refrigerationHandlers.handlers },
+  { view: 'threePhasePower', label: '三相功率计算', icon: 'icon-three-phase', iconSrc: '/assets/icons/zap-white.png', calculator: threePhasePowerCalculator, handlers: threePhasePowerHandlers.handlers }
 ]
 ]
+const TOOL_ENTRIES = TOOL_MODULES.map((item) => ({
+  icon: item.icon,
+  iconSrc: item.iconSrc,
+  label: item.label,
+  view: item.view
+}))
 
 
 function getToolEntries() {
 function getToolEntries() {
   return TOOL_ENTRIES.map((item) => ({ ...item }))
   return TOOL_ENTRIES.map((item) => ({ ...item }))
@@ -46,15 +52,14 @@ function getToolTitle(view) {
 }
 }
 
 
 function createToolInitialState() {
 function createToolInitialState() {
-  return {
-    ...crcTool.createInitialState(),
-    ...asciiCodeTool.createInitialState(),
-    ...filterCalculator.createInitialState(),
-    ...smdCodeCalculator.createInitialState(),
-    ...refrigerationCalculator.createInitialState(),
-    ...reactanceCalculator.createInitialState(),
-    ...threePhasePowerCalculator.createInitialState()
-  }
+  return TOOL_MODULES.reduce((state, item) => {
+    if (!item.calculator || typeof item.calculator.createInitialState !== 'function') return state
+
+    return {
+      ...state,
+      ...item.calculator.createInitialState()
+    }
+  }, {})
 }
 }
 
 
 const toolNavigation = {
 const toolNavigation = {
@@ -64,38 +69,45 @@ const toolNavigation = {
   isToolView
   isToolView
 }
 }
 
 
-const toolPageHandlers = {
-  copyToolResult(event) {
-    const value = event && event.currentTarget && event.currentTarget.dataset
-      ? event.currentTarget.dataset.value
-      : ''
-    const text = String(value === undefined || value === null ? '' : value).trim()
-
-    if (!text || text === '--') return
-
-    const wxApi = getWxApi()
-    if (typeof wxApi.setClipboardData !== 'function') {
-      if (this.pageToast) this.pageToast.show('当前环境不支持复制', 'error')
-      return
+function copyToolResult(event) {
+  const value = event && event.currentTarget && event.currentTarget.dataset
+    ? event.currentTarget.dataset.value
+    : ''
+  const text = String(value === undefined || value === null ? '' : value).trim()
+
+  if (!text || text === '--') return
+
+  const wxApi = getWxApi()
+  if (typeof wxApi.setClipboardData !== 'function') {
+    if (this.pageToast) this.pageToast.show('当前环境不支持复制', 'error')
+    return
+  }
+
+  wxApi.setClipboardData({
+    data: text,
+    fail: () => {
+      if (this.pageToast) this.pageToast.show('复制失败', 'error')
+    },
+    success: () => {
+      if (this.pageToast) this.pageToast.show('已复制')
     }
     }
+  })
+}
 
 
-    wxApi.setClipboardData({
-      data: text,
-      fail: () => {
-        if (this.pageToast) this.pageToast.show('复制失败', 'error')
-      },
-      success: () => {
-        if (this.pageToast) this.pageToast.show('已复制')
-      }
-    })
-  },
-  ...crcHandlers.handlers,
-  ...asciiCodeHandlers.handlers,
-  ...filterHandlers.handlers,
-  ...reactanceHandlers.handlers,
-  ...smdCodeHandlers.handlers,
-  ...refrigerationHandlers.handlers,
-  ...threePhasePowerHandlers.handlers
+function createToolHandlers() {
+  return TOOL_MODULES.reduce((handlers, item) => {
+    if (!item.handlers) return handlers
+
+    return {
+      ...handlers,
+      ...item.handlers
+    }
+  }, {})
+}
+
+const toolPageHandlers = {
+  copyToolResult,
+  ...createToolHandlers()
 }
 }
 
 
 module.exports = {
 module.exports = {

+ 5 - 5
protocols/modbus-rtu/index.js

@@ -2,7 +2,9 @@ const {
   padHex
   padHex
 } = require('../../utils/base-utils.js')
 } = require('../../utils/base-utils.js')
 const {
 const {
-  bytesToWords
+  bytesToHex,
+  bytesToWords,
+  splitUint16BE
 } = require('../../utils/binary-utils.js')
 } = require('../../utils/binary-utils.js')
 const {
 const {
   BYTE_ORDER_LOW,
   BYTE_ORDER_LOW,
@@ -50,9 +52,7 @@ function toWord(value, label) {
   return value
   return value
 }
 }
 
 
-function splitWord(value) {
-  return [(value >> 8) & 0xFF, value & 0xFF]
-}
+const splitWord = splitUint16BE
 
 
 function normalizeMaxFrameBytes(maxFrameBytes = MAX_MODBUS_DMA_BYTES) {
 function normalizeMaxFrameBytes(maxFrameBytes = MAX_MODBUS_DMA_BYTES) {
   const numberValue = Number(maxFrameBytes)
   const numberValue = Number(maxFrameBytes)
@@ -153,7 +153,7 @@ function buildWriteMultipleRegistersFrame(slaveAddress, address, values, options
 }
 }
 
 
 function formatHex(bytes) {
 function formatHex(bytes) {
-  return bytes.map((byte) => byte.toString(16).padStart(2, '0').toUpperCase()).join(' ')
+  return bytesToHex(bytes, ' ')
 }
 }
 
 
 function getReadResponseByteLength(functionCode, quantity) {
 function getReadResponseByteLength(functionCode, quantity) {

+ 33 - 44
protocols/storage-access/index.js

@@ -2,11 +2,18 @@ const {
   padHex
   padHex
 } = require('../../utils/base-utils.js')
 } = require('../../utils/base-utils.js')
 const {
 const {
-  bytesToWords,
+  bytesToHex,
+  bytesToWordsLE,
+  formatHexNumber,
+  readByte,
+  readUint16LE,
+  readUint32LE,
+  splitUint16LE,
+  splitUint32LE,
   toByteArray
   toByteArray
 } = require('../../utils/binary-utils.js')
 } = require('../../utils/binary-utils.js')
 const {
 const {
-  BYTE_ORDER_HIGH,
+  BYTE_ORDER_LOW,
   appendCrc16Ccitt,
   appendCrc16Ccitt,
   crc16Ccitt,
   crc16Ccitt,
   hasValidCrc16Ccitt
   hasValidCrc16Ccitt
@@ -89,7 +96,7 @@ const WRITE_RESPONSE_LENGTH_16 = 1 + ADDRESS16_BYTE_LENGTH + 2 + 2
 const WRITE_RESPONSE_LENGTH_32 = 1 + ADDRESS32_BYTE_LENGTH + 2 + 2
 const WRITE_RESPONSE_LENGTH_32 = 1 + ADDRESS32_BYTE_LENGTH + 2 + 2
 const EXCEPTION_RESPONSE_LENGTH = 4
 const EXCEPTION_RESPONSE_LENGTH = 4
 const CODE_INFO_DESCRIPTOR_ADDRESS = 0
 const CODE_INFO_DESCRIPTOR_ADDRESS = 0
-const CODE_INFO_DESCRIPTOR_BYTE_LENGTH = 9
+const CODE_INFO_DESCRIPTOR_BYTE_LENGTH = 11
 const CODE_INFO_DESCRIPTOR_RESPONSE_LENGTH = 1 + CODE_INFO_DESCRIPTOR_BYTE_LENGTH + 2
 const CODE_INFO_DESCRIPTOR_RESPONSE_LENGTH = 1 + CODE_INFO_DESCRIPTOR_BYTE_LENGTH + 2
 const MEMORY_ENDIAN_MARK_BIG = 0x55AA
 const MEMORY_ENDIAN_MARK_BIG = 0x55AA
 const MEMORY_ENDIAN_MARK_LITTLE = 0xAA55
 const MEMORY_ENDIAN_MARK_LITTLE = 0xAA55
@@ -99,12 +106,16 @@ const MEMORY_ENDIAN = {
 }
 }
 
 
 const STORAGE_CRC_OPTIONS = {
 const STORAGE_CRC_OPTIONS = {
-  byteOrder: BYTE_ORDER_HIGH
+  byteOrder: BYTE_ORDER_LOW
 }
 }
 const VALID_AREAS = [AREA.CODEINFO, AREA.ADDR32, AREA.DATA, AREA.IDATA, AREA.XDATA, AREA.CODE]
 const VALID_AREAS = [AREA.CODEINFO, AREA.ADDR32, AREA.DATA, AREA.IDATA, AREA.XDATA, AREA.CODE]
 const MEMORY_AREAS = [AREA.CODEINFO, AREA.ADDR32, AREA.DATA, AREA.IDATA, AREA.XDATA, AREA.CODE]
 const MEMORY_AREAS = [AREA.CODEINFO, AREA.ADDR32, AREA.DATA, AREA.IDATA, AREA.XDATA, AREA.CODE]
 const WRITABLE_AREAS = [AREA.ADDR32, AREA.DATA, AREA.IDATA, AREA.XDATA]
 const WRITABLE_AREAS = [AREA.ADDR32, AREA.DATA, AREA.IDATA, AREA.XDATA]
 const RESERVED_AREAS = [0x05, 0x06]
 const RESERVED_AREAS = [0x05, 0x06]
+const readWord = readUint16LE
+const readDword = readUint32LE
+const splitWord = splitUint16LE
+const splitDword = splitUint32LE
 
 
 function toByte(value, label) {
 function toByte(value, label) {
   if (!Number.isInteger(value) || value < 0 || value > 0xFF) {
   if (!Number.isInteger(value) || value < 0 || value > 0xFF) {
@@ -201,34 +212,6 @@ function toByteLength(value, label = '字节长度', maxPayload = MAX_PAYLOAD_BY
   return byteLength
   return byteLength
 }
 }
 
 
-function splitWord(value) {
-  return [(value >> 8) & 0xFF, value & 0xFF]
-}
-
-function splitDword(value) {
-  const normalizedValue = Number(value) >>> 0
-
-  return [
-    (normalizedValue >>> 24) & 0xFF,
-    (normalizedValue >>> 16) & 0xFF,
-    (normalizedValue >>> 8) & 0xFF,
-    normalizedValue & 0xFF
-  ]
-}
-
-function readWord(bytes, offset) {
-  return (((bytes[offset] || 0) << 8) | (bytes[offset + 1] || 0)) & 0xFFFF
-}
-
-function readDword(bytes, offset) {
-  return (
-    ((bytes[offset] || 0) * 0x1000000)
-    + (((bytes[offset + 1] || 0) << 16) >>> 0)
-    + (((bytes[offset + 2] || 0) << 8) >>> 0)
-    + (bytes[offset + 3] || 0)
-  ) >>> 0
-}
-
 function normalizeDescriptorAddressWidth(value) {
 function normalizeDescriptorAddressWidth(value) {
   const numberValue = Number(value)
   const numberValue = Number(value)
   if (numberValue === 16 || numberValue === 32) return numberValue
   if (numberValue === 16 || numberValue === 32) return numberValue
@@ -237,16 +220,18 @@ function normalizeDescriptorAddressWidth(value) {
 }
 }
 
 
 function parseMemoryEndianMark(bytes, offset) {
 function parseMemoryEndianMark(bytes, offset) {
-  const marker = readWord(bytes, offset)
-  if (marker === MEMORY_ENDIAN_MARK_BIG) {
+  const firstByte = readByte(bytes, offset)
+  const secondByte = readByte(bytes, offset + 1)
+  const marker = ((firstByte << 8) | secondByte) & 0xFFFF
+  if (firstByte === 0x55 && secondByte === 0xAA) {
     return {
     return {
-      marker,
+      marker: MEMORY_ENDIAN_MARK_BIG,
       memoryEndian: MEMORY_ENDIAN.BIG
       memoryEndian: MEMORY_ENDIAN.BIG
     }
     }
   }
   }
-  if (marker === MEMORY_ENDIAN_MARK_LITTLE) {
+  if (firstByte === 0xAA && secondByte === 0x55) {
     return {
     return {
-      marker,
+      marker: MEMORY_ENDIAN_MARK_LITTLE,
       memoryEndian: MEMORY_ENDIAN.LITTLE
       memoryEndian: MEMORY_ENDIAN.LITTLE
     }
     }
   }
   }
@@ -340,7 +325,7 @@ function hasValidStorageCrc(bytes) {
   if (frame.length >= 4) return hasValidCrc16Ccitt(frame, STORAGE_CRC_OPTIONS)
   if (frame.length >= 4) return hasValidCrc16Ccitt(frame, STORAGE_CRC_OPTIONS)
 
 
   const expected = crc16Ccitt(frame.slice(0, -2), STORAGE_CRC_OPTIONS)
   const expected = crc16Ccitt(frame.slice(0, -2), STORAGE_CRC_OPTIONS)
-  const received = (((frame[frame.length - 2] || 0) << 8) | (frame[frame.length - 1] || 0)) & 0xFFFF
+  const received = readWord(frame, frame.length - 2)
   return expected === received
   return expected === received
 }
 }
 
 
@@ -403,6 +388,10 @@ function parseCodeInfoDescriptorBytes(bytes) {
   if (dataBytes.length < CODE_INFO_DESCRIPTOR_BYTE_LENGTH) {
   if (dataBytes.length < CODE_INFO_DESCRIPTOR_BYTE_LENGTH) {
     throw new Error('CodeInfo 描述符长度无效')
     throw new Error('CodeInfo 描述符长度无效')
   }
   }
+  const endianMark = parseMemoryEndianMark(dataBytes, 9)
+  if (!endianMark.memoryEndian) {
+    throw new Error('CodeInfo 描述符字节序标记无效')
+  }
 
 
   return {
   return {
     codeInfoAddress: readDword(dataBytes, 0),
     codeInfoAddress: readDword(dataBytes, 0),
@@ -410,13 +399,13 @@ function parseCodeInfoDescriptorBytes(bytes) {
     codeInfoByteLength: readWord(dataBytes, 4),
     codeInfoByteLength: readWord(dataBytes, 4),
     codeInfoDescriptorBytes: dataBytes.slice(0, CODE_INFO_DESCRIPTOR_BYTE_LENGTH),
     codeInfoDescriptorBytes: dataBytes.slice(0, CODE_INFO_DESCRIPTOR_BYTE_LENGTH),
     codeInfoMaxPacketLength: readWord(dataBytes, 7),
     codeInfoMaxPacketLength: readWord(dataBytes, 7),
-    codeInfoMemoryEndian: MEMORY_ENDIAN.BIG,
-    codeInfoMemoryEndianMark: 0
+    codeInfoMemoryEndian: endianMark.memoryEndian,
+    codeInfoMemoryEndianMark: endianMark.marker
   }
   }
 }
 }
 
 
 function formatHex(bytes) {
 function formatHex(bytes) {
-  return toByteArray(bytes).map((byte) => byte.toString(16).padStart(2, '0').toUpperCase()).join(' ')
+  return bytesToHex(bytes, ' ')
 }
 }
 
 
 function parseStorageAccessResponse(bytes) {
 function parseStorageAccessResponse(bytes) {
@@ -461,7 +450,7 @@ function parseStorageAccessResponse(bytes) {
       isAddress32: false,
       isAddress32: false,
       isWrite: false,
       isWrite: false,
       protocol: PROTOCOL_NAME,
       protocol: PROTOCOL_NAME,
-      words: bytesToWords(dataBytes.length % 2 === 0 ? dataBytes : dataBytes.concat(0))
+      words: bytesToWordsLE(dataBytes.length % 2 === 0 ? dataBytes : dataBytes.concat(0))
     }
     }
 
 
     return {
     return {
@@ -513,7 +502,7 @@ function parseStorageAccessResponse(bytes) {
     isAddress32: decoded.isAddress32,
     isAddress32: decoded.isAddress32,
     isWrite: false,
     isWrite: false,
     protocol: PROTOCOL_NAME,
     protocol: PROTOCOL_NAME,
-    words: bytesToWords(dataBytes.length % 2 === 0 ? dataBytes : dataBytes.concat(0))
+    words: bytesToWordsLE(dataBytes.length % 2 === 0 ? dataBytes : dataBytes.concat(0))
   }
   }
 
 
   return response
   return response
@@ -843,7 +832,7 @@ function createControlExpected(operation, kind = 'storage-control', options = {}
 }
 }
 
 
 function formatAddress(value) {
 function formatAddress(value) {
-  return Number(value || 0).toString(16).toUpperCase()
+  return formatHexNumber(value, 1).replace(/^0+(?=[0-9A-F])/, '')
 }
 }
 
 
 function getChunkLabel(label, chunks, chunk) {
 function getChunkLabel(label, chunks, chunk) {

+ 109 - 1
utils/binary-utils.js

@@ -34,6 +34,79 @@ function bytesToHex(bytes, separator = '') {
   return toByteArray(bytes).map((byte) => (byte & 0xFF).toString(16).toUpperCase().padStart(2, '0')).join(separator)
   return toByteArray(bytes).map((byte) => (byte & 0xFF).toString(16).toUpperCase().padStart(2, '0')).join(separator)
 }
 }
 
 
+function readByte(bytes, offset) {
+  if (!bytes) return 0
+  if (bytes instanceof ArrayBuffer) return (new Uint8Array(bytes)[offset] || 0) & 0xFF
+
+  return (bytes[offset] || 0) & 0xFF
+}
+
+function formatHexNumber(value, length = 2) {
+  const numberValue = Math.max(0, Math.floor(Number(value) || 0))
+
+  return numberValue.toString(16).toUpperCase().padStart(length, '0')
+}
+
+function readUint16BE(bytes = [], offset = 0) {
+  return ((readByte(bytes, offset) << 8) | readByte(bytes, offset + 1)) & 0xFFFF
+}
+
+function readUint16LE(bytes = [], offset = 0) {
+  return (readByte(bytes, offset) | (readByte(bytes, offset + 1) << 8)) & 0xFFFF
+}
+
+function readUint32BE(bytes = [], offset = 0) {
+  return (
+    (readByte(bytes, offset) * 0x1000000)
+    + ((readByte(bytes, offset + 1) << 16) >>> 0)
+    + ((readByte(bytes, offset + 2) << 8) >>> 0)
+    + readByte(bytes, offset + 3)
+  ) >>> 0
+}
+
+function readUint32LE(bytes = [], offset = 0) {
+  return (
+    readByte(bytes, offset)
+    + ((readByte(bytes, offset + 1) << 8) >>> 0)
+    + ((readByte(bytes, offset + 2) << 16) >>> 0)
+    + (readByte(bytes, offset + 3) * 0x1000000)
+  ) >>> 0
+}
+
+function splitUint16BE(value) {
+  const numberValue = Number(value) & 0xFFFF
+
+  return [(numberValue >> 8) & 0xFF, numberValue & 0xFF]
+}
+
+function splitUint16LE(value) {
+  const numberValue = Number(value) & 0xFFFF
+
+  return [numberValue & 0xFF, (numberValue >> 8) & 0xFF]
+}
+
+function splitUint32BE(value) {
+  const numberValue = Number(value) >>> 0
+
+  return [
+    (numberValue >>> 24) & 0xFF,
+    (numberValue >>> 16) & 0xFF,
+    (numberValue >>> 8) & 0xFF,
+    numberValue & 0xFF
+  ]
+}
+
+function splitUint32LE(value) {
+  const numberValue = Number(value) >>> 0
+
+  return [
+    numberValue & 0xFF,
+    (numberValue >>> 8) & 0xFF,
+    (numberValue >>> 16) & 0xFF,
+    (numberValue >>> 24) & 0xFF
+  ]
+}
+
 function bytesToAsciiText(bytes = []) {
 function bytesToAsciiText(bytes = []) {
   return String.fromCharCode.apply(null, trimTrailingNullBytes(bytes).map((byte) => byte & 0xFF))
   return String.fromCharCode.apply(null, trimTrailingNullBytes(bytes).map((byte) => byte & 0xFF))
 }
 }
@@ -75,6 +148,18 @@ function bytesToWords(bytes = []) {
   return words
   return words
 }
 }
 
 
+function bytesToWordsLE(bytes = []) {
+  const words = []
+
+  for (let index = 0; index + 1 < bytes.length; index += 2) {
+    const lowByte = bytes[index] || 0
+    const highByte = bytes[index + 1] || 0
+    words.push(((highByte << 8) | lowByte) & 0xFFFF)
+  }
+
+  return words
+}
+
 function getByteFromWord(word, byteOffset = 0) {
 function getByteFromWord(word, byteOffset = 0) {
   const value = Number(word) & 0xFFFF
   const value = Number(word) & 0xFFFF
 
 
@@ -119,6 +204,17 @@ function wordsToBytes(words = [], byteLength = words.length * 2) {
   return bytes.slice(0, Math.max(0, byteLength))
   return bytes.slice(0, Math.max(0, byteLength))
 }
 }
 
 
+function wordsToBytesLE(words = [], byteLength = words.length * 2) {
+  const bytes = []
+
+  for (let index = 0; index < words.length; index += 1) {
+    const word = Number(words[index]) & 0xFFFF
+    bytes.push(word & 0xFF, (word >> 8) & 0xFF)
+  }
+
+  return bytes.slice(0, Math.max(0, byteLength))
+}
+
 module.exports = {
 module.exports = {
   bytesToBase64,
   bytesToBase64,
   bytesToBin,
   bytesToBin,
@@ -126,10 +222,22 @@ module.exports = {
   bytesToAsciiText,
   bytesToAsciiText,
   bytesToUtf8Text,
   bytesToUtf8Text,
   bytesToWords,
   bytesToWords,
+  bytesToWordsLE,
   formatBytes,
   formatBytes,
+  formatHexNumber,
   getByteFromWord,
   getByteFromWord,
+  readByte,
+  readUint16BE,
+  readUint16LE,
+  readUint32BE,
+  readUint32LE,
+  splitUint16BE,
+  splitUint16LE,
+  splitUint32BE,
+  splitUint32LE,
   stringToUtf8Bytes,
   stringToUtf8Bytes,
   toByteArray,
   toByteArray,
   trimTrailingNullBytes,
   trimTrailingNullBytes,
-  wordsToBytes
+  wordsToBytes,
+  wordsToBytesLE
 }
 }

+ 8 - 6
协议架构说明.md

@@ -44,8 +44,9 @@ BLE 透传链路
 1. 一个协议只保留一个协议入口文件,不再拆 `frame.js`、`request.js`、`response.js` 这类细粒度文件。
 1. 一个协议只保留一个协议入口文件,不再拆 `frame.js`、`request.js`、`response.js` 这类细粒度文件。
 2. 功能服务按页面或业务域聚合,只有 UI 状态、协议传输、数据同步边界清晰时才拆分模块。
 2. 功能服务按页面或业务域聚合,只有 UI 状态、协议传输、数据同步边界清晰时才拆分模块。
 3. 领域模型可以按“值类型、编解码、结构体解析、参数组规范化”拆分,因为这些逻辑可测试且复用度高。
 3. 领域模型可以按“值类型、编解码、结构体解析、参数组规范化”拆分,因为这些逻辑可测试且复用度高。
-4. 页面不直接拼协议帧,不直接读写本地存储格式。
-5. 设置页的协议模式只保留当前字段 `protocolMode`,不维护历史字段迁移逻辑。
+4. 二进制读写、HEX 格式化和字节数组归一化统一放在 `utils/binary-utils.js`,协议层不重复手写字节拆装。
+5. 页面不直接拼协议帧,不直接读写本地存储格式。
+6. 设置页的协议模式只保留当前字段 `protocolMode`,不维护历史字段迁移逻辑。
 
 
 ## 2. 协议模式
 ## 2. 协议模式
 
 
@@ -103,8 +104,8 @@ BLE 透传链路
 - `CMD bit3` 为普通读写位,`bit4/bit5` 保留,`bit6` 为特殊指令位,`bit7` 为回帧故障标志。
 - `CMD bit3` 为普通读写位,`bit4/bit5` 保留,`bit6` 为特殊指令位,`bit7` 为回帧故障标志。
 - 普通区域包括 `CODEINFO`、`DATA`、`IDATA`、`XDATA`、`CODE`、`ADDR32`,其中 `CODEINFO` 和 `CODE` 只读。
 - 普通区域包括 `CODEINFO`、`DATA`、`IDATA`、`XDATA`、`CODE`、`ADDR32`,其中 `CODEINFO` 和 `CODE` 只读。
 - 特殊指令使用 `CMD=0x40 | special_code`,当前仅定义 `special_code=0x01` 复位。
 - 特殊指令使用 `CMD=0x40 | special_code`,当前仅定义 `special_code=0x01` 复位。
-- CodeInfo 同步先发送 `00 + CRC` 读取 `area=0x00 CODEINFO`,获取 `TLV_ADDR32 + TLV_LEN16 + ADDR_WIDTH8 + MAX_PACKET16`,再按 `ADDR_WIDTH` 用 CODE 或 ADDR32 读取完整信息块。
-- 协议控制字段始终固定大端;结构体字段和单独变量等目标内存值当前默认按大端编解码
+- CodeInfo 同步先发送 `00 + CRC` 读取 `area=0x00 CODEINFO`,获取小端序 `TLV_ADDR32 + TLV_LEN16 + ADDR_WIDTH8 + MAX_PACKET16` 和原始两字节 `ENDIAN_MARK16`,再按 `ADDR_WIDTH` 用 CODE 或 ADDR32 读取完整信息块。
+- 协议控制字段始终固定小端;结构体字段和单独变量等目标内存多字节值根据 `ENDIAN_MARK` 自动按大端或小端编解码,未同步时默认小端
 - 不直接调用 BLE 发送,不负责弹窗;普通内存协议 IO 由 `features/storage-access/protocol-io.js` 编排。
 - 不直接调用 BLE 发送,不负责弹窗;普通内存协议 IO 由 `features/storage-access/protocol-io.js` 编排。
 
 
 完整帧格式和从机实现参考见 `存储访问协议.md`。
 完整帧格式和从机实现参考见 `存储访问协议.md`。
@@ -162,7 +163,7 @@ BLE 透传链路
 - 解析 CodeInfo 纯 TLV 信息块,未知 TLV 类型跳过。
 - 解析 CodeInfo 纯 TLV 信息块,未知 TLV 类型跳过。
 - 使用 CODEINFO 描述符返回的 `TLV_LEN` 决定 CodeInfo 总长度,并使用 `ADDR_WIDTH/MAX_PACKET` 作为同步上下文;内存入口地址宽度和区域由 TLV `TYPE` 自描述。
 - 使用 CODEINFO 描述符返回的 `TLV_LEN` 决定 CodeInfo 总长度,并使用 `ADDR_WIDTH/MAX_PACKET` 作为同步上下文;内存入口地址宽度和区域由 TLV `TYPE` 自描述。
 - 解析 UTF-8/ASCII 电机型号、芯片型号和转换相关可选 TLV 参数。
 - 解析 UTF-8/ASCII 电机型号、芯片型号和转换相关可选 TLV 参数。
-- 固定 TLV `0x01~0x08` 映射真实存储区域、地址宽度和结构体/变量类型,并按结构体、单独变量成对排列;VALUE 为 `addr(2/4) + byte_len16 + name_len8 + name`。
+- 固定 TLV `0x01~0x08` 映射真实存储区域、地址宽度和结构体/变量类型,并按结构体、单独变量成对排列;VALUE 为小端序 `addr(2/4) + byte_len16 + name_len8 + name`。
 - `0x20~0x3F` 为自定义 TLV,板卡参数从 `0x40` 开始递增。
 - `0x20~0x3F` 为自定义 TLV,板卡参数从 `0x40` 开始递增。
 - 生成参数组初始结构,结构体未导入定义时按字节占位,单独变量按 TLV `byte_len` 显示为未配置原始字节,后续由 UI 或 enum 导入确定同长度的解释类型。
 - 生成参数组初始结构,结构体未导入定义时按字节占位,单独变量按 TLV `byte_len` 显示为未配置原始字节,后续由 UI 或 enum 导入确定同长度的解释类型。
 
 
@@ -192,6 +193,7 @@ BLE 透传链路
 | 文件 | 职责 |
 | 文件 | 职责 |
 |---|---|
 |---|---|
 | `protocol-io.js` | 普通内存读写、特殊指令发送、协议分包和设备协议包长 |
 | `protocol-io.js` | 普通内存读写、特殊指令发送、协议分包和设备协议包长 |
+| `memory-areas.js` | 存储访问区域元数据、手动命令区域选项、只读区域判断和名称到区域码映射 |
 | `manual-command.js` | 通讯页手动读写表单状态、校验、预览和执行;不暴露 CODEINFO 描述符读取 |
 | `manual-command.js` | 通讯页手动读写表单状态、校验、预览和执行;不暴露 CODEINFO 描述符读取 |
 | `code-info-sync.js` | 同步按钮触发的 CodeInfo 描述符读取、TLV 信息块读取、解析和参数组导入模型生成 |
 | `code-info-sync.js` | 同步按钮触发的 CodeInfo 描述符读取、TLV 信息块读取、解析和参数组导入模型生成 |
 
 
@@ -276,7 +278,7 @@ CodeInfo 读取只从同步流程进入:先读取 `area=0x00 CODEINFO` 描述
   -> features/storage-access/code-info-sync.readCodeInfoBlock
   -> features/storage-access/code-info-sync.readCodeInfoBlock
   -> features/storage-access/protocol-io.readMemory
   -> features/storage-access/protocol-io.readMemory
   -> protocols/storage-access/index.js 构建普通读帧
   -> protocols/storage-access/index.js 构建普通读帧
-  -> 发送 00 + CRC 读取 area=0x00 CODEINFO 描述符,获得 TLV_ADDR32 + TLV_LEN16 + ADDR_WIDTH8 + MAX_PACKET16
+  -> 发送 00 + CRC 读取 area=0x00 CODEINFO 描述符,获得小端序 TLV_ADDR32 + TLV_LEN16 + ADDR_WIDTH8 + MAX_PACKET16 和原始 ENDIAN_MARK16
   -> 按描述符和设置页最大包长分片读取 CodeInfo TLV 信息块
   -> 按描述符和设置页最大包长分片读取 CodeInfo TLV 信息块
   -> domain/storage-access/code-info-parser.parseCodeInfo
   -> domain/storage-access/code-info-parser.parseCodeInfo
   -> domain/storage-access/code-info-parser.createGroupsFromCodeInfo
   -> domain/storage-access/code-info-parser.createGroupsFromCodeInfo

+ 40 - 38
存储访问协议.md

@@ -9,16 +9,16 @@
 - 标准 Modbus RTU 使用从机地址、功能码和 Modbus CRC。
 - 标准 Modbus RTU 使用从机地址、功能码和 Modbus CRC。
 - 存储访问协议使用 `CMD + ADDR + LEN + DATA + CRC16-CCITT-FALSE`。
 - 存储访问协议使用 `CMD + ADDR + LEN + DATA + CRC16-CCITT-FALSE`。
 - 复位使用 `bit6=1` 的特殊指令帧;当前仅定义复位特殊指令。
 - 复位使用 `bit6=1` 的特殊指令帧;当前仅定义复位特殊指令。
-- CodeInfo 同步先读取普通区域 `area=0x00` 的 codeinfo 描述符,再用 `addr32` 读取 TLV 信息块。
+- CodeInfo 同步先读取普通区域 `area=0x00` 的 codeinfo 描述符,再按描述符地址位宽读取 TLV 信息块。
 
 
 ## 2. 字节序与 CRC
 ## 2. 字节序与 CRC
 
 
-除 CRC 算法内部计算外,所有多字节协议控制字段均为大端序。当前目标内存变量的多字节值默认按大端解释和编辑
+除 CRC 算法内部计算外,所有多字节协议控制字段均为小端序。目标内存变量的多字节值在 CodeInfo 同步后按 `ENDIAN_MARK` 自动选择大端或小端;未同步或未声明时默认小端
 
 
 ```text
 ```text
-32 位地址 : ADDR_3 ADDR_2 ADDR_1 ADDR_0
-16 位长度 : LEN_H LEN_L
-CRC 输出  : CRC_H CRC_L
+32 位地址 : ADDR_0 ADDR_1 ADDR_2 ADDR_3
+16 位长度 : LEN_L LEN_H
+CRC 输出  : CRC_L CRC_H
 ```
 ```
 
 
 CRC 使用 `CRC16-CCITT-FALSE`:
 CRC 使用 `CRC16-CCITT-FALSE`:
@@ -30,7 +30,7 @@ CRC 使用 `CRC16-CCITT-FALSE`:
 | 输入反转 | 否 |
 | 输入反转 | 否 |
 | 输出反转 | 否 |
 | 输出反转 | 否 |
 | 结果异或 | `0x0000` |
 | 结果异或 | `0x0000` |
-| 输出顺序 | 字节在前 |
+| 输出顺序 | 字节在前 |
 
 
 CRC 覆盖除最后两个 CRC 字节外的整帧内容。
 CRC 覆盖除最后两个 CRC 字节外的整帧内容。
 
 
@@ -92,8 +92,8 @@ bit2~bit0 AREA      普通读写区域码
 ### 5.1 读请求
 ### 5.1 读请求
 
 
 ```text
 ```text
-MODE=0x07: CMD ADDR_3 ADDR_2 ADDR_1 ADDR_0 LEN_H LEN_L CRC_H CRC_L
-MODE=0x01..0x04: CMD ADDR_H ADDR_L LEN_H LEN_L CRC_H CRC_L
+AREA=0x07: CMD ADDR_0 ADDR_1 ADDR_2 ADDR_3 LEN_L LEN_H CRC_L CRC_H
+AREA=0x01..0x04: CMD ADDR_L ADDR_H LEN_L LEN_H CRC_L CRC_H
 ```
 ```
 
 
 长度分别为 9 字节或 7 字节。
 长度分别为 9 字节或 7 字节。
@@ -101,8 +101,8 @@ MODE=0x01..0x04: CMD ADDR_H ADDR_L LEN_H LEN_L CRC_H CRC_L
 ### 5.2 写请求
 ### 5.2 写请求
 
 
 ```text
 ```text
-MODE=0x07: CMD ADDR_3 ADDR_2 ADDR_1 ADDR_0 LEN_H LEN_L DATA... CRC_H CRC_L
-MODE=0x01..0x04: CMD ADDR_H ADDR_L LEN_H LEN_L DATA... CRC_H CRC_L
+AREA=0x07: CMD ADDR_0 ADDR_1 ADDR_2 ADDR_3 LEN_L LEN_H DATA... CRC_L CRC_H
+AREA=0x01..0x04: CMD ADDR_L ADDR_H LEN_L LEN_H DATA... CRC_L CRC_H
 ```
 ```
 
 
 长度分别为 `9 + LEN` 或 `7 + LEN` 字节。
 长度分别为 `9 + LEN` 或 `7 + LEN` 字节。
@@ -110,8 +110,8 @@ MODE=0x01..0x04: CMD ADDR_H ADDR_L LEN_H LEN_L DATA... CRC_H CRC_L
 ### 5.3 正常读响应
 ### 5.3 正常读响应
 
 
 ```text
 ```text
-MODE=0x07: CMD ADDR_3 ADDR_2 ADDR_1 ADDR_0 LEN_H LEN_L DATA... CRC_H CRC_L
-MODE=0x01..0x04: CMD ADDR_H ADDR_L LEN_H LEN_L DATA... CRC_H CRC_L
+AREA=0x07: CMD ADDR_0 ADDR_1 ADDR_2 ADDR_3 LEN_L LEN_H DATA... CRC_L CRC_H
+AREA=0x01..0x04: CMD ADDR_L ADDR_H LEN_L LEN_H DATA... CRC_L CRC_H
 ```
 ```
 
 
 长度分别为 `9 + LEN` 或 `7 + LEN` 字节。
 长度分别为 `9 + LEN` 或 `7 + LEN` 字节。
@@ -121,8 +121,8 @@ MODE=0x01..0x04: CMD ADDR_H ADDR_L LEN_H LEN_L DATA... CRC_H CRC_L
 ### 5.4 正常写响应
 ### 5.4 正常写响应
 
 
 ```text
 ```text
-MODE=0x07: CMD ADDR_3 ADDR_2 ADDR_1 ADDR_0 LEN_H LEN_L CRC_H CRC_L
-MODE=0x01..0x03: CMD ADDR_H ADDR_L LEN_H LEN_L CRC_H CRC_L
+AREA=0x07: CMD ADDR_0 ADDR_1 ADDR_2 ADDR_3 LEN_L LEN_H CRC_L CRC_H
+AREA=0x01..0x04: CMD ADDR_L ADDR_H LEN_L LEN_H CRC_L CRC_H
 ```
 ```
 
 
 长度分别为 9 字节或 7 字节。
 长度分别为 9 字节或 7 字节。
@@ -132,7 +132,7 @@ MODE=0x01..0x03: CMD ADDR_H ADDR_L LEN_H LEN_L CRC_H CRC_L
 ### 5.5 异常响应
 ### 5.5 异常响应
 
 
 ```text
 ```text
-CMD_ERR EXCEPTION_CODE CRC_H CRC_L
+CMD_ERR EXCEPTION_CODE CRC_L CRC_H
 ```
 ```
 
 
 长度固定 4 字节。`CMD_ERR` 为原请求 `CMD | 0x80`。
 长度固定 4 字节。`CMD_ERR` 为原请求 `CMD | 0x80`。
@@ -142,9 +142,9 @@ CMD_ERR EXCEPTION_CODE CRC_H CRC_L
 特殊指令使用 `CMD bit6=1`,`bit0~bit5` 表示特殊指令码。当前仅定义 `0x01` 复位。
 特殊指令使用 `CMD bit6=1`,`bit0~bit5` 表示特殊指令码。当前仅定义 `0x01` 复位。
 
 
 ```text
 ```text
-请求: CMD DATA... CRC_H CRC_L
-响应: CMD DATA... CRC_H CRC_L
-异常: CMD_ERR EXCEPTION_CODE CRC_H CRC_L
+请求: CMD DATA... CRC_L CRC_H
+响应: CMD DATA... CRC_L CRC_H
+异常: CMD_ERR EXCEPTION_CODE CRC_L CRC_H
 ```
 ```
 
 
 特殊指令执行失败时直接返回异常帧,成功响应不额外携带状态字节。
 特殊指令执行失败时直接返回异常帧,成功响应不额外携带状态字节。
@@ -174,7 +174,7 @@ CMD_ERR EXCEPTION_CODE CRC_H CRC_L
 
 
 CodeInfo 同步分两步:
 CodeInfo 同步分两步:
 
 
-1. 读取普通区域 `area=0x00 CODEINFO` 的描述符,响应 DATA 为 9 字节。
+1. 读取普通区域 `area=0x00 CODEINFO` 的描述符,响应 DATA 为 11 字节。
 2. 按描述符返回的 TLV 起始地址、`len16`、地址位宽和最大包长,读取完整 CodeInfo TLV 信息块。
 2. 按描述符返回的 TLV 起始地址、`len16`、地址位宽和最大包长,读取完整 CodeInfo TLV 信息块。
 
 
 ### 8.1 读取 CODEINFO 描述符
 ### 8.1 读取 CODEINFO 描述符
@@ -182,13 +182,13 @@ CodeInfo 同步分两步:
 请求:
 请求:
 
 
 ```text
 ```text
-00 CRC_H CRC_L
+00 CRC_L CRC_H
 ```
 ```
 
 
 从机成功响应:
 从机成功响应:
 
 
 ```text
 ```text
-00 TLV_ADDR_3 TLV_ADDR_2 TLV_ADDR_1 TLV_ADDR_0 TLV_LEN_H TLV_LEN_L ADDR_WIDTH MAX_PACKET_H MAX_PACKET_L CRC_H CRC_L
+00 TLV_ADDR_0 TLV_ADDR_1 TLV_ADDR_2 TLV_ADDR_3 TLV_LEN_L TLV_LEN_H ADDR_WIDTH MAX_PACKET_L MAX_PACKET_H ENDIAN_MARK_0 ENDIAN_MARK_1 CRC_L CRC_H
 ```
 ```
 
 
 响应 DATA 字段:
 响应 DATA 字段:
@@ -199,8 +199,9 @@ CodeInfo 同步分两步:
 | `TLV_LEN` | 2 | CodeInfo TLV 信息块的字节长度 |
 | `TLV_LEN` | 2 | CodeInfo TLV 信息块的字节长度 |
 | `ADDR_WIDTH` | 1 | 读取 CodeInfo 信息块本体时使用的地址位宽,只允许 `16` 或 `32` |
 | `ADDR_WIDTH` | 1 | 读取 CodeInfo 信息块本体时使用的地址位宽,只允许 `16` 或 `32` |
 | `MAX_PACKET` | 2 | 从机允许的最大完整协议帧长度,包含 CMD/ADDR/LEN/DATA/CRC;为 0 表示未声明 |
 | `MAX_PACKET` | 2 | 从机允许的最大完整协议帧长度,包含 CMD/ADDR/LEN/DATA/CRC;为 0 表示未声明 |
+| `ENDIAN_MARK` | 2 | 目标内存值字节序标记;按字节读取为 `55 AA` 表示大端,`AA 55` 表示小端 |
 
 
-描述符区域 `CODEINFO` 只读,不支持写入。`CMD`、`ADDR`、普通读写 `LEN`、`TLV_LEN`、`MAX_PACKET` 等多字节协议控制字段始终按大端序解析;TLV `LEN` 为单字节字段,不涉及大小端。
+描述符区域 `CODEINFO` 只读,不支持写入。`CMD`、`ADDR`、普通读写 `LEN`、`TLV_LEN`、`MAX_PACKET`、固定内存入口 `byte_addr/byte_len` 等多字节协议控制字段始终按小端序解析;TLV `LEN` 与 `name_len` 为单字节字段,不涉及大小端。`ENDIAN_MARK` 不作为协议控制数值转换,而作为原始两字节同步标记:按字节读取为 `55 AA` 表示目标内存多字节值大端,`AA 55` 表示目标内存多字节值小端。
 
 
 ### 8.2 读取完整 CodeInfo
 ### 8.2 读取完整 CodeInfo
 
 
@@ -259,8 +260,8 @@ TYPE LEN VALUE...
 
 
 | 字段 | 长度 | 说明 |
 | 字段 | 长度 | 说明 |
 |---|---:|---|
 |---|---:|---|
-| `byte_addr` | 2 | 结构体实例或单独变量所在区域的字节地址 |
-| `byte_len` | 2 | 结构体实例或单独变量的字节长度 |
+| `byte_addr` | 2 | 结构体实例或单独变量所在区域的字节地址,小端序 |
+| `byte_len` | 2 | 结构体实例或单独变量的字节长度,小端序 |
 | `name_len` | 1 | `name` 字节长度 |
 | `name_len` | 1 | `name` 字节长度 |
 | `name` | `name_len` | 动态名称字段,UTF-8 或 ASCII;结构体定义名、变量名或 enum 类型名 |
 | `name` | `name_len` | 动态名称字段,UTF-8 或 ASCII;结构体定义名、变量名或 enum 类型名 |
 
 
@@ -270,8 +271,8 @@ TYPE LEN VALUE...
 
 
 | 字段 | 长度 | 说明 |
 | 字段 | 长度 | 说明 |
 |---|---:|---|
 |---|---:|---|
-| `byte_addr` | 4 | 结构体实例或单独变量所在统一地址空间内的字节地址 |
-| `byte_len` | 2 | 结构体实例或单独变量的字节长度 |
+| `byte_addr` | 4 | 结构体实例或单独变量所在统一地址空间内的字节地址,小端序 |
+| `byte_len` | 2 | 结构体实例或单独变量的字节长度,小端序 |
 | `name_len` | 1 | `name` 字节长度 |
 | `name_len` | 1 | `name` 字节长度 |
 | `name` | `name_len` | 动态名称字段,UTF-8 或 ASCII;结构体定义名、变量名或 enum 类型名 |
 | `name` | `name_len` | 动态名称字段,UTF-8 或 ASCII;结构体定义名、变量名或 enum 类型名 |
 
 
@@ -290,9 +291,9 @@ TYPE LEN VALUE...
 | `0x40` | `cave_freq` | 1 | `uint8_t` | 载波频率,单位 KHz |
 | `0x40` | `cave_freq` | 1 | `uint8_t` | 载波频率,单位 KHz |
 | `0x41` | `ref_volt` | 1 | `uint8_t` | 基准电压实际值乘 10,显示时除以 10,单位 V |
 | `0x41` | `ref_volt` | 1 | `uint8_t` | 基准电压实际值乘 10,显示时除以 10,单位 V |
 | `0x42` | `amp_gain` | 1 | `uint8_t` | 运放倍数,无单位 |
 | `0x42` | `amp_gain` | 1 | `uint8_t` | 运放倍数,无单位 |
-| `0x43` | `rs_shunt` | 2 | `uint16_t` | 采样电阻,单位 mΩ |
-| `0x44` | `bus_div` | 4 | `float32` | 母线电压分压比,大端序 |
-| `0x45` | `along_div` | 4 | `float32` | 模拟输入电压分压比,大端序 |
+| `0x43` | `rs_shunt` | 2 | `uint16_t` | 采样电阻,单位 mΩ,按 `ENDIAN_MARK` 解析 |
+| `0x44` | `bus_div` | 4 | `float32` | 母线电压分压比,按 `ENDIAN_MARK` 解析 |
+| `0x45` | `along_div` | 4 | `float32` | 模拟输入电压分压比,按 `ENDIAN_MARK` 解析 |
 | `0x46` | `chip_model` | 可变 | UTF-8 或 ASCII 字符串 | 芯片型号,建议 0 结尾或 0 填充 |
 | `0x46` | `chip_model` | 可变 | UTF-8 或 ASCII 字符串 | 芯片型号,建议 0 结尾或 0 填充 |
 | `0x47` | `model` | 可变 | UTF-8 或 ASCII 字符串 | 电机型号,建议最多 30 字节,可容纳至少 7 个常见 UTF-8 汉字 |
 | `0x47` | `model` | 可变 | UTF-8 或 ASCII 字符串 | 电机型号,建议最多 30 字节,可容纳至少 7 个常见 UTF-8 汉字 |
 
 
@@ -305,7 +306,7 @@ TYPE LEN VALUE...
 3. 遇到固定结构体入口 `TYPE=0x01/0x03/0x05/0x07` 创建结构体组;结构体定义未导入时按字节占位。
 3. 遇到固定结构体入口 `TYPE=0x01/0x03/0x05/0x07` 创建结构体组;结构体定义未导入时按字节占位。
 4. 遇到固定变量入口 `TYPE=0x02/0x04/0x06/0x08` 创建单独变量组,初始按 `byte_len` 显示原始字节并标记为未配置。
 4. 遇到固定变量入口 `TYPE=0x02/0x04/0x06/0x08` 创建单独变量组,初始按 `byte_len` 显示原始字节并标记为未配置。
 5. 按固定 TLV `TYPE` 解析内存区域和地址宽度。
 5. 按固定 TLV `TYPE` 解析内存区域和地址宽度。
-6. 当前目标内存多字节值默认按大端解释;单字节值不受影响。
+6. 根据描述符 `ENDIAN_MARK` 自动确定目标内存多字节值字节序:`55 AA` 为大端,`AA 55` 为小端;单字节值不受影响。
 7. 根据 `byte_addr`、`byte_len`、`TYPE`、`name` 创建参数组。
 7. 根据 `byte_addr`、`byte_len`、`TYPE`、`name` 创建参数组。
 8. 如果导入了 C 结构体和 enum 定义,只有结构体入口且 `name` 与结构体定义名一致、长度一致时才补全结构体字段;结构体字段类型为 enum 时,读回值按枚举项显示。
 8. 如果导入了 C 结构体和 enum 定义,只有结构体入口且 `name` 与结构体定义名一致、长度一致时才补全结构体字段;结构体字段类型为 enum 时,读回值按枚举项显示。
 9. 单独变量入口需要在 UI 中选择与 `byte_len` 一致的有符号/无符号整数或 `float` 类型;如果 `name` 匹配 enum 类型名,或导入文件中存在 `EnumType variable_name;` 且变量名匹配 `name`,读回值按枚举项显示。
 9. 单独变量入口需要在 UI 中选择与 `byte_len` 一致的有符号/无符号整数或 `float` 类型;如果 `name` 匹配 enum 类型名,或导入文件中存在 `EnumType variable_name;` 且变量名匹配 `name`,读回值按枚举项显示。
@@ -320,8 +321,8 @@ TYPE LEN VALUE...
 AREA = 0x03 XDATA
 AREA = 0x03 XDATA
 ADDR = 0x2000
 ADDR = 0x2000
 LEN = 0x0040
 LEN = 0x0040
-读请求 = 03 20 00 00 40 CRC_H CRC_L
-正常响应 = 03 20 00 00 40 DATA(64B) CRC_H CRC_L
+读请求 = 03 00 20 40 00 CRC_L CRC_H
+正常响应 = 03 00 20 40 00 DATA(64B) CRC_L CRC_H
 ```
 ```
 
 
 ### 11.2 读取 CodeInfo
 ### 11.2 读取 CodeInfo
@@ -333,18 +334,19 @@ TLV_ADDR = 0x00123456
 TLV_LEN  = 0x0120
 TLV_LEN  = 0x0120
 ADDR_WIDTH = 32
 ADDR_WIDTH = 32
 MAX_PACKET = 0x0040
 MAX_PACKET = 0x0040
+ENDIAN_MARK = AA 55
 ```
 ```
 
 
 先读取 `CODEINFO` 描述符:
 先读取 `CODEINFO` 描述符:
 
 
 ```text
 ```text
-00 CRC_H CRC_L
+00 CRC_L CRC_H
 ```
 ```
 
 
 再按 `ADDR_WIDTH=32` 读取 TLV 信息块:
 再按 `ADDR_WIDTH=32` 读取 TLV 信息块:
 
 
 ```text
 ```text
-07 00 12 34 56 01 20 CRC_H CRC_L
+07 56 34 12 00 20 01 CRC_L CRC_H
 ```
 ```
 
 
 `0x07` 表示 32 位统一地址模式。
 `0x07` 表示 32 位统一地址模式。
@@ -356,7 +358,7 @@ MAX_PACKET = 0x0040
 ```text
 ```text
 TYPE = 0x05
 TYPE = 0x05
 LEN = 0x14
 LEN = 0x14
-VALUE = 20 00 00 40 0F 4D 6F 74 6F 72 5F 52 75 6E 74 69 6D 65 5F 74
+VALUE = 00 20 40 00 0F 4D 6F 74 6F 72 5F 52 75 6E 74 69 6D 65 5F 74
 含义 = XDATA 0x2000 / 64 bytes / Motor_Runtime_t
 含义 = XDATA 0x2000 / 64 bytes / Motor_Runtime_t
 ```
 ```
 
 
@@ -365,14 +367,14 @@ VALUE = 20 00 00 40 0F 4D 6F 74 6F 72 5F 52 75 6E 74 69 6D 65 5F 74
 ```text
 ```text
 TYPE = 0x08
 TYPE = 0x08
 LEN = 0x10
 LEN = 0x10
-VALUE = 00 00 21 00 00 02 09 73 70 65 65 64 5F 72 65 66
+VALUE = 00 21 00 00 02 00 09 73 70 65 65 64 5F 72 65 66
 含义 = ADDR32 0x00002100 / 2 bytes / speed_ref
 含义 = ADDR32 0x00002100 / 2 bytes / speed_ref
 ```
 ```
 
 
 ## 12. 实现约束
 ## 12. 实现约束
 
 
 1. 普通内存读写帧的 `LEN` 单位始终为字节,字段固定 16 位,有效范围为 `1..0xFFFF`;CodeInfo TLV 项内的 `LEN` 为 1 字节,只表示单项 `VALUE` 长度。
 1. 普通内存读写帧的 `LEN` 单位始终为字节,字段固定 16 位,有效范围为 `1..0xFFFF`;CodeInfo TLV 项内的 `LEN` 为 1 字节,只表示单项 `VALUE` 长度。
-2. `ADDR` 单位始终为字节;`MODE=0x07` 时地址字段为 32 位,`MODE=0x01..0x04` 时地址字段为 16 位;`MODE=0x00` 为 CODEINFO 短描述符请求;`MODE=0x05..0x06` 保留。
+2. `ADDR` 单位始终为字节;`AREA=0x07` 时地址字段为 32 位,`AREA=0x01..0x04` 时地址字段为 16 位;`AREA=0x00` 为 CODEINFO 短描述符请求;`AREA=0x05..0x06` 保留。
 3. 普通读写响应必须回显请求的 `CMD`、`ADDR`、`LEN`。
 3. 普通读写响应必须回显请求的 `CMD`、`ADDR`、`LEN`。
 4. 上位机必须校验响应 `CMD`、`ADDR`、`LEN`、CRC,并确认数据长度等于 `LEN`。
 4. 上位机必须校验响应 `CMD`、`ADDR`、`LEN`、CRC,并确认数据长度等于 `LEN`。
 5. `CODEINFO` 和 `CODE` 区只读,写入必须返回 `WRITE_PROTECT`。
 5. `CODEINFO` 和 `CODE` 区只读,写入必须返回 `WRITE_PROTECT`。
@@ -398,4 +400,4 @@ VALUE = 00 00 21 00 00 02 09 73 70 65 65 64 5F 72 65 66
 | `ADDR` | 4 字节 | typed 扩展建议使用 32 位字节地址 |
 | `ADDR` | 4 字节 | typed 扩展建议使用 32 位字节地址 |
 | `VALUE_WIDTH` | `0x01`、`0x02`、`0x04` | 单个值 8/16/32 位 |
 | `VALUE_WIDTH` | `0x01`、`0x02`、`0x04` | 单个值 8/16/32 位 |
 | `COUNT` | 16 位 | 值数量,不是字节数 |
 | `COUNT` | 16 位 | 值数量,不是字节数 |
-| `DATA` | `VALUE_WIDTH * COUNT` 字节 | 按协议端序 |
+| `DATA` | `VALUE_WIDTH * COUNT` 字节 | 按协议端序 |

+ 19 - 19
完整协议说明.md

@@ -84,10 +84,10 @@ SLAVE (FUNC | 0x80) EXCEPTION_CODE CRC_L CRC_H
 存储访问协议不带从机地址,不属于标准 Modbus。普通内存访问帧格式为:
 存储访问协议不带从机地址,不属于标准 Modbus。普通内存访问帧格式为:
 
 
 ```text
 ```text
-CMD ADDR LEN DATA... CRC_H CRC_L
+CMD ADDR LEN DATA... CRC_L CRC_H
 ```
 ```
 
 
-CRC 使用 `CRC16-CCITT-FALSE`,高字节在前。所有多字节协议字段均为大端序。
+CRC 使用 `CRC16-CCITT-FALSE`,低字节在前。所有多字节协议控制字段均为小端序。
 
 
 `CMD` 位定义:
 `CMD` 位定义:
 
 
@@ -96,12 +96,12 @@ bit7      ERR       故障标志,仅回帧出现
 bit6      SPECIAL   特殊指令标志位
 bit6      SPECIAL   特殊指令标志位
 bit5~bit4 RSV       保留,普通读写保持 0
 bit5~bit4 RSV       保留,普通读写保持 0
 bit3      RW        普通读写标志,0=读,1=写
 bit3      RW        普通读写标志,0=读,1=写
-bit2~bit0 MODE      普通读写区域码
+bit2~bit0 AREA      普通读写区域码
 ```
 ```
 
 
-地址模式与存储区域:
+地址区域与存储区域:
 
 
-| MODE | 名称 | 地址宽度 | 读 | 写 | 说明 |
+| AREA | 名称 | 地址宽度 | 读 | 写 | 说明 |
 |---:|---|---|---|---|---|
 |---:|---|---|---|---|---|
 | `0x00` | CODEINFO | 16 位 | 支持 | 禁止 | 同步时读取一次,返回 TLV 起始地址与长度 |
 | `0x00` | CODEINFO | 16 位 | 支持 | 禁止 | 同步时读取一次,返回 TLV 起始地址与长度 |
 | `0x01` | DATA | 16 位 | 支持 | 支持 | 内部直接寻址 RAM |
 | `0x01` | DATA | 16 位 | 支持 | 支持 | 内部直接寻址 RAM |
@@ -114,9 +114,9 @@ bit2~bit0 MODE      普通读写区域码
 普通读请求:
 普通读请求:
 
 
 ```text
 ```text
-CMD ADDR_3 ADDR_2 ADDR_1 ADDR_0 LEN_H LEN_L CRC_H CRC_L
+CMD ADDR_0 ADDR_1 ADDR_2 ADDR_3 LEN_L LEN_H CRC_L CRC_H
-CMD ADDR_H ADDR_L LEN_H LEN_L CRC_H CRC_L
+CMD ADDR_L ADDR_H LEN_L LEN_H CRC_L CRC_H
 ```
 ```
 
 
 普通读响应回显 `CMD`、`ADDR`、`LEN`,然后携带 `LEN` 字节数据。
 普通读响应回显 `CMD`、`ADDR`、`LEN`,然后携带 `LEN` 字节数据。
@@ -124,9 +124,9 @@ CMD ADDR_H ADDR_L LEN_H LEN_L CRC_H CRC_L
 普通写请求:
 普通写请求:
 
 
 ```text
 ```text
-CMD ADDR_3 ADDR_2 ADDR_1 ADDR_0 LEN_H LEN_L DATA... CRC_H CRC_L
+CMD ADDR_0 ADDR_1 ADDR_2 ADDR_3 LEN_L LEN_H DATA... CRC_L CRC_H
-CMD ADDR_H ADDR_L LEN_H LEN_L DATA... CRC_H CRC_L
+CMD ADDR_L ADDR_H LEN_L LEN_H DATA... CRC_L CRC_H
 ```
 ```
 
 
 普通写响应只回显 `CMD`、`ADDR`、`LEN`。写 CODEINFO 或 CODE 区必须返回写保护异常。
 普通写响应只回显 `CMD`、`ADDR`、`LEN`。写 CODEINFO 或 CODE 区必须返回写保护异常。
@@ -134,9 +134,9 @@ CMD ADDR_H ADDR_L LEN_H LEN_L DATA... CRC_H CRC_L
 特殊指令使用 `CMD bit6=1`:
 特殊指令使用 `CMD bit6=1`:
 
 
 ```text
 ```text
-请求: CMD DATA... CRC_H CRC_L
-响应: CMD DATA... CRC_H CRC_L
-异常: CMD_ERR EXCEPTION_CODE CRC_H CRC_L
+请求: CMD DATA... CRC_L CRC_H
+响应: CMD DATA... CRC_L CRC_H
+异常: CMD_ERR EXCEPTION_CODE CRC_L CRC_H
 ```
 ```
 
 
 特殊指令表:
 特殊指令表:
@@ -151,10 +151,10 @@ CMD ADDR_H ADDR_L LEN_H LEN_L DATA... CRC_H CRC_L
 
 
 存储访问同步固定分两步:
 存储访问同步固定分两步:
 
 
-1. 发送 `00 CRC_H CRC_L` 读取 `area=0x00 CODEINFO` 描述符,数据区为 `TLV_ADDR32 + TLV_LEN16 + ADDR_WIDTH8 + MAX_PACKET16`。
+1. 发送 `00 CRC_L CRC_H` 读取 `area=0x00 CODEINFO` 描述符,数据区为 `TLV_ADDR32 + TLV_LEN16 + ADDR_WIDTH8 + MAX_PACKET16 + ENDIAN_MARK16`。
 2. 根据描述符返回的地址、长度、地址位宽和最大包长读取完整 CodeInfo TLV 信息块;`ADDR_WIDTH=16` 使用 `CODE` 区 16 位地址,`ADDR_WIDTH=32` 使用 `ADDR32`。
 2. 根据描述符返回的地址、长度、地址位宽和最大包长读取完整 CodeInfo TLV 信息块;`ADDR_WIDTH=16` 使用 `CODE` 区 16 位地址,`ADDR_WIDTH=32` 使用 `ADDR32`。
 
 
-多字节协议控制字段始终固定大端,包括 `CMD`、`ADDR`、普通读写 `LEN`、描述符 `TLV_LEN` 和 `MAX_PACKET`;TLV `LEN` 为单字节字段,不涉及大小端。当前目标内存变量的多字节值默认按大端解码和写入
+多字节协议控制字段始终固定小端,包括 `ADDR`、普通读写 `LEN`、描述符 `TLV_ADDR/TLV_LEN/MAX_PACKET` 和固定内存入口 `byte_addr/byte_len`;TLV `LEN` 与 `name_len` 为单字节字段,不涉及大小端。描述符末尾 `ENDIAN_MARK` 按原始两字节识别目标内存值字节序:`55 AA` 为大端,`AA 55` 为小端;大端时目标内存多字节值按大端解析,小端时参数组多字节值自动按小端编解码
 
 
 CodeInfo 信息块使用纯 TLV 格式,不再包含固定头、`format_version`、`board_info_format`、`struct_entry_len` 或 `struct_table`:
 CodeInfo 信息块使用纯 TLV 格式,不再包含固定头、`format_version`、`board_info_format`、`struct_entry_len` 或 `struct_table`:
 
 
@@ -177,7 +177,7 @@ TYPE LEN VALUE...
 | `0x20..0x3F` | 自定义 TLV | 上位机保留原始项,当前跳过业务解析 |
 | `0x20..0x3F` | 自定义 TLV | 上位机保留原始项,当前跳过业务解析 |
 | `0x40..` | 板卡参数 | `0x40` 起按 `cave_freq/ref_volt/...` 递增 |
 | `0x40..` | 板卡参数 | `0x40` 起按 `cave_freq/ref_volt/...` 递增 |
 
 
-TLV `LEN` 为 1 字节,表示单项 `VALUE` 字节数;整段 CodeInfo 总长度仍由 CODEINFO 描述符 `TLV_LEN16` 决定。内存入口 TLV 的地址宽度和区域由 `TYPE` 决定,名称字段由 `name_len` 声明,名称本身为 UTF-8 或 ASCII 字节,不再固定 32 字节。
+TLV `LEN` 为 1 字节,表示单项 `VALUE` 字节数;整段 CodeInfo 总长度仍由 CODEINFO 描述符 `TLV_LEN16` 决定。内存入口 TLV 的地址宽度和区域由 `TYPE` 决定,`VALUE` 中的 `byte_addr/byte_len` 按小端序解析,名称字段由 `name_len` 声明,名称本身为 UTF-8 或 ASCII 字节,不再固定 32 字节。
 
 
 同步到参数页后的规则:
 同步到参数页后的规则:
 
 
@@ -198,7 +198,7 @@ CodeInfo 卡片始终显示在通讯页存储访问协议卡片内。板卡信
 
 
 参数页读回的数据先按字段类型解码为原始值,再根据可选转换公式显示实际值。公式由参数配置自定义,适合标幺值转实际值、比例缩放和偏置换算。
 参数页读回的数据先按字段类型解码为原始值,再根据可选转换公式显示实际值。公式由参数配置自定义,适合标幺值转实际值、比例缩放和偏置换算。
 
 
-存储访问参数组的多字节变量值当前默认按大端解码和写入。标准 Modbus 寄存器仍保持 Modbus 字/字节规则,不受该字段影响。
+存储访问参数组的多字节变量值按 CodeInfo 描述符 `ENDIAN_MARK` 自动选择大端或小端解码和写入;未同步 CodeInfo 时默认小端。标准 Modbus 寄存器仍保持 Modbus 字/字节规则,不受该字段影响。
 
 
 公式支持变量:
 公式支持变量:
 
 
@@ -212,8 +212,8 @@ CodeInfo 卡片始终显示在通讯页存储访问协议卡片内。板卡信
 | `busDiv` | CodeInfo 母线电压分压比 |
 | `busDiv` | CodeInfo 母线电压分压比 |
 | `alongDiv` | CodeInfo 模拟输入电压分压比 |
 | `alongDiv` | CodeInfo 模拟输入电压分压比 |
 | `maxPacketLength` | 当前同步上下文的最大完整协议帧长度,未声明时为 0 |
 | `maxPacketLength` | 当前同步上下文的最大完整协议帧长度,未声明时为 0 |
-| `addressWidth` | CodeInfo 本体读取地址长度,当前固定为 32 |
-| `memoryEndian` | 当前目标内存变量字节序,默认 `big` |
+| `addressWidth` | CodeInfo 本体读取地址长度,来自描述符 `ADDR_WIDTH` |
+| `memoryEndian` | 当前目标内存变量字节序,来自描述符 `ENDIAN_MARK`;未同步时默认 `little` |
 
 
 公式只支持数字、括号和 `+ - * /`,不执行任意脚本。
 公式只支持数字、括号和 `+ - * /`,不执行任意脚本。
 
 
@@ -246,6 +246,6 @@ ACK 为 `0x06`,NAK 为 `0x15`。固件文件加载、芯片型号识别、升
 
 
 1. 标准 Modbus、存储访问、Bootloader 分别只保留一个协议入口:`protocols/modbus-rtu/index.js`、`protocols/storage-access/index.js`、`protocols/bootloader/index.js`。
 1. 标准 Modbus、存储访问、Bootloader 分别只保留一个协议入口:`protocols/modbus-rtu/index.js`、`protocols/storage-access/index.js`、`protocols/bootloader/index.js`。
 2. 页面只负责 UI 展示和事件转发,不直接拼帧、不直接解析二进制响应。
 2. 页面只负责 UI 展示和事件转发,不直接拼帧、不直接解析二进制响应。
-3. 通讯页业务聚合在 `features/communication/`,参数页业务聚合在 `features/parameter-groups/`,存储访问按 `features/storage-access/manual-command.js`、`features/storage-access/protocol-io.js`、`features/storage-access/code-info-sync.js` 分离 UI 状态、协议传输和数据同步。
+3. 通讯页业务聚合在 `features/communication/`,参数页业务聚合在 `features/parameter-groups/`,存储访问按 `features/storage-access/manual-command.js`、`features/storage-access/memory-areas.js`、`features/storage-access/protocol-io.js`、`features/storage-access/code-info-sync.js` 分离 UI 状态、区域元数据、协议传输和数据同步。
 4. 可复用且有明确领域边界的逻辑保留在 `domain/parameter-groups/` 和 `domain/storage-access/`。
 4. 可复用且有明确领域边界的逻辑保留在 `domain/parameter-groups/` 和 `domain/storage-access/`。
 5. 不再为单个按钮、单个字段、单个导入步骤新增小模块;新增能力优先并入现有协议模块、功能服务或领域模型。
 5. 不再为单个按钮、单个字段、单个导入步骤新增小模块;新增能力优先并入现有协议模块、功能服务或领域模型。