You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

317 lines
11 KiB

  1. #!/usr/bin/env python
  2. #
  3. # Copyright 2021 Don Kjer
  4. #
  5. # This program is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU General Public License as published by
  7. # the Free Software Foundation, either version 2 of the License, or
  8. # (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. #
  18. from __future__ import print_function
  19. import argparse
  20. from struct import pack, unpack
  21. import os, sys
  22. MAGIC_FEEA = '\xea\xff\xfe\xff'
  23. MAGIC_FEE9 = '\x16\x01'
  24. EMPTY_WORD = '\xff\xff'
  25. WORD_ENCODING = 0x8000
  26. VALUE_NEXT = 0x6000
  27. VALUE_RESERVED = 0x4000
  28. VALUE_ENCODED = 0x2000
  29. BYTE_RANGE = 0x80
  30. CHUNK_SIZE = 1024
  31. STRUCT_FMTS = {
  32. 1: 'B',
  33. 2: 'H',
  34. 4: 'I'
  35. }
  36. PRINTABLE='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ '
  37. EECONFIG_V1 = [
  38. ("MAGIC", 0, 2),
  39. ("DEBUG", 2, 1),
  40. ("DEFAULT_LAYER", 3, 1),
  41. ("KEYMAP", 4, 1),
  42. ("MOUSEKEY_ACCEL", 5, 1),
  43. ("BACKLIGHT", 6, 1),
  44. ("AUDIO", 7, 1),
  45. ("RGBLIGHT", 8, 4),
  46. ("UNICODEMODE", 12, 1),
  47. ("STENOMODE", 13, 1),
  48. ("HANDEDNESS", 14, 1),
  49. ("KEYBOARD", 15, 4),
  50. ("USER", 19, 4),
  51. ("VELOCIKEY", 23, 1),
  52. ("HAPTIC", 24, 4),
  53. ("MATRIX", 28, 4),
  54. ("MATRIX_EXTENDED", 32, 2),
  55. ("KEYMAP_UPPER_BYTE", 34, 1),
  56. ]
  57. VIABASE_V1 = 35
  58. VERBOSE = False
  59. def parseArgs():
  60. parser = argparse.ArgumentParser(description='Decode an STM32 emulated eeprom dump')
  61. parser.add_argument('-s', '--size', type=int,
  62. help='Size of the emulated eeprom (default: input_size / 2)')
  63. parser.add_argument('-o', '--output', help='File to write decoded eeprom to')
  64. parser.add_argument('-y', '--layout-options-size', type=int,
  65. help='VIA layout options size (default: 1)', default=1)
  66. parser.add_argument('-t', '--custom-config-size', type=int,
  67. help='VIA custom config size (default: 0)', default=0)
  68. parser.add_argument('-l', '--layers', type=int,
  69. help='VIA keyboard layers (default: 4)', default=4)
  70. parser.add_argument('-r', '--rows', type=int, help='VIA matrix rows')
  71. parser.add_argument('-c', '--cols', type=int, help='VIA matrix columns')
  72. parser.add_argument('-m', '--macros', type=int,
  73. help='VIA macro count (default: 16)', default=16)
  74. parser.add_argument('-C', '--canonical', action='store_true',
  75. help='Canonical hex+ASCII display.')
  76. parser.add_argument('-v', '--verbose', action='store_true', help='Verbose output')
  77. parser.add_argument('input', help='Raw contents of the STM32 flash area used to emulate eeprom')
  78. return parser.parse_args()
  79. def decodeEepromFEEA(in_file, size):
  80. decoded=size*[None]
  81. pos = 0
  82. while True:
  83. chunk = in_file.read(CHUNK_SIZE)
  84. for i in range(0, len(chunk), 2):
  85. decoded[pos] = unpack('B', chunk[i])[0]
  86. pos += 1
  87. if pos >= size:
  88. break
  89. if len(chunk) < CHUNK_SIZE or pos >= size:
  90. break
  91. return decoded
  92. def decodeEepromFEE9(in_file, size):
  93. decoded=size*[None]
  94. pos = 0
  95. # Read compacted flash
  96. while True:
  97. read_size = min(size - pos, CHUNK_SIZE)
  98. chunk = in_file.read(read_size)
  99. for i in range(len(chunk)):
  100. decoded[pos] = unpack('B', chunk[i])[0] ^ 0xFF
  101. pos += 1
  102. if pos >= size:
  103. break
  104. if len(chunk) < read_size or pos >= size:
  105. break
  106. if VERBOSE:
  107. print("COMPACTED EEPROM:")
  108. dumpBinary(decoded, True)
  109. print("WRITE LOG:")
  110. # Read write log
  111. while True:
  112. entry = in_file.read(2)
  113. if len(entry) < 2:
  114. print("Partial log address at position 0x%04x" % pos, file=sys.stderr)
  115. break
  116. pos += 2
  117. if entry == EMPTY_WORD:
  118. break
  119. be_entry = unpack('>H', entry)[0]
  120. entry = unpack('H', entry)[0]
  121. if not (entry & WORD_ENCODING):
  122. address = entry >> 8
  123. decoded[address] = entry & 0xFF
  124. if VERBOSE:
  125. print("[0x%04x]: BYTE 0x%02x = 0x%02x" % (be_entry, address, decoded[address]))
  126. else:
  127. if (entry & VALUE_NEXT) == VALUE_NEXT:
  128. # Read next word as value
  129. value = in_file.read(2)
  130. if len(value) < 2:
  131. print("Partial log value at position 0x%04x" % pos, file=sys.stderr)
  132. break
  133. pos += 2
  134. address = entry & 0x1FFF
  135. address <<= 1
  136. address += BYTE_RANGE
  137. decoded[address] = unpack('B', value[0])[0] ^ 0xFF
  138. decoded[address+1] = unpack('B', value[1])[0] ^ 0xFF
  139. be_value = unpack('>H', value)[0]
  140. if VERBOSE:
  141. print("[0x%04x 0x%04x]: WORD 0x%04x = 0x%02x%02x" % (be_entry, be_value, address, decoded[address+1], decoded[address]))
  142. else:
  143. # Reserved for future use
  144. if entry & VALUE_RESERVED:
  145. if VERBOSE:
  146. print("[0x%04x]: RESERVED 0x%04x" % (be_entry, address))
  147. continue
  148. address = entry & 0x1FFF
  149. address <<= 1
  150. decoded[address] = (entry & VALUE_ENCODED) >> 13
  151. decoded[address+1] = 0
  152. if VERBOSE:
  153. print("[0x%04x]: ENCODED 0x%04x = 0x%02x%02x" % (be_entry, address, decoded[address+1], decoded[address]))
  154. return decoded
  155. def dumpBinary(data, canonical):
  156. def display(pos, row):
  157. print("%04x" % pos, end='')
  158. for i in range(len(row)):
  159. if i % 8 == 0:
  160. print(" ", end='')
  161. char = row[i]
  162. if char is None:
  163. print(" ", end='')
  164. else:
  165. print(" %02x" % row[i], end='')
  166. if canonical:
  167. print(" |", end='')
  168. for i in range(len(row)):
  169. char = row[i]
  170. if char is None:
  171. char = " "
  172. else:
  173. char = chr(char)
  174. if char not in PRINTABLE:
  175. char = "."
  176. print(char, end='')
  177. print("|", end='')
  178. print("")
  179. size = len(data)
  180. empty_rows = 0
  181. prev_row = ''
  182. first_repeat = True
  183. for pos in range(0, size, 16):
  184. row=data[pos:pos+16]
  185. row[len(row):16] = (16-len(row))*[None]
  186. if row == prev_row:
  187. if first_repeat:
  188. print("*")
  189. first_repeat = False
  190. else:
  191. first_repeat = True
  192. display(pos, row)
  193. prev_row = row
  194. print("%04x" % (pos+16))
  195. def dumpEeconfig(data, eeconfig):
  196. print("EECONFIG:")
  197. for (name, pos, length) in eeconfig:
  198. fmt = STRUCT_FMTS[length]
  199. value = unpack(fmt, ''.join([chr(x) for x in data[pos:pos+length]]))[0]
  200. print(("%%04x %%s = 0x%%0%dx" % (length * 2)) % (pos, name, value))
  201. def dumpVia(data, base, layers, cols, rows, macros,
  202. layout_options_size, custom_config_size):
  203. magicYear = data[base + 0]
  204. magicMonth = data[base + 1]
  205. magicDay = data[base + 2]
  206. # Sanity check
  207. if not 10 <= magicYear <= 0x99 or \
  208. not 0 <= magicMonth <= 0x12 or \
  209. not 0 <= magicDay <= 0x31:
  210. print("ERROR: VIA Signature is not valid; Year:%x, Month:%x, Day:%x" % (magicYear, magicMonth, magicDay))
  211. return
  212. if cols is None or rows is None:
  213. print("ERROR: VIA dump requires specifying --rows and --cols", file=sys.stderr)
  214. return 2
  215. print("VIA:")
  216. # Decode magic
  217. print("%04x MAGIC = 20%02x-%02x-%02x" % (base, magicYear, magicMonth, magicDay))
  218. # Decode layout options
  219. options = 0
  220. pos = base + 3
  221. for i in range(base+3, base+3+layout_options_size):
  222. options = options << 8
  223. options |= data[i]
  224. print(("%%04x LAYOUT_OPTIONS = 0x%%0%dx" % (layout_options_size * 2)) % (pos, options))
  225. pos += layout_options_size + custom_config_size
  226. # Decode keycodes
  227. keymap_size = layers * rows * cols * 2
  228. if (pos + keymap_size) >= (len(data) - 1):
  229. print("ERROR: VIA keymap requires %d bytes, but only %d available" % (keymap_size, len(data) - pos))
  230. return 3
  231. for layer in range(layers):
  232. print("%s LAYER %d %s" % ('-'*int(cols*2.5), layer, '-'*int(cols*2.5)))
  233. for row in range(rows):
  234. print("%04x | " % pos, end='')
  235. for col in range(cols):
  236. keycode = (data[pos] << 8) | (data[pos+1])
  237. print(" %04x" % keycode, end='')
  238. pos += 2
  239. print("")
  240. # Decode macros
  241. for macro_num in range(macros):
  242. macro = ""
  243. macro_pos = pos
  244. while pos < len(data):
  245. char = chr(data[pos])
  246. pos += 1
  247. if char == '\x00':
  248. print("%04x MACRO[%d] = '%s'" % (macro_pos, macro_num, macro))
  249. break
  250. else:
  251. macro += char
  252. return 0
  253. def decodeSTM32Eeprom(input, canonical, size=None, output=None, **kwargs):
  254. input_size = os.path.getsize(input)
  255. if size is None:
  256. size = input_size >> 1
  257. # Read the first few bytes to check magic signature
  258. with open(input, 'rb') as in_file:
  259. magic=in_file.read(4)
  260. in_file.seek(0)
  261. if magic == MAGIC_FEEA:
  262. decoded = decodeEepromFEEA(in_file, size)
  263. eeconfig = EECONFIG_V1
  264. via_base = VIABASE_V1
  265. elif magic[:2] == MAGIC_FEE9:
  266. decoded = decodeEepromFEE9(in_file, size)
  267. eeconfig = EECONFIG_V1
  268. via_base = VIABASE_V1
  269. else:
  270. print("Unknown magic signature: %s" % " ".join(["0x%02x" % ord(x) for x in magic]), file=sys.stderr)
  271. return 1
  272. if output is not None:
  273. with open(output, 'wb') as out_file:
  274. out_file.write(pack('%dB' % len(decoded), *decoded))
  275. print("DECODED EEPROM:")
  276. dumpBinary(decoded, canonical)
  277. dumpEeconfig(decoded, eeconfig)
  278. if kwargs['rows'] is not None and kwargs['cols'] is not None:
  279. return dumpVia(decoded, via_base, **kwargs)
  280. return 0
  281. def main():
  282. global VERBOSE
  283. kwargs = vars(parseArgs())
  284. VERBOSE = kwargs.pop('verbose')
  285. return decodeSTM32Eeprom(**kwargs)
  286. if __name__ == '__main__':
  287. sys.exit(main())