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.

316 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. prev_row = ''
  181. first_repeat = True
  182. for pos in range(0, size, 16):
  183. row=data[pos:pos+16]
  184. row[len(row):16] = (16-len(row))*[None]
  185. if row == prev_row:
  186. if first_repeat:
  187. print("*")
  188. first_repeat = False
  189. else:
  190. first_repeat = True
  191. display(pos, row)
  192. prev_row = row
  193. print("%04x" % (pos+16))
  194. def dumpEeconfig(data, eeconfig):
  195. print("EECONFIG:")
  196. for (name, pos, length) in eeconfig:
  197. fmt = STRUCT_FMTS[length]
  198. value = unpack(fmt, ''.join([chr(x) for x in data[pos:pos+length]]))[0]
  199. print(("%%04x %%s = 0x%%0%dx" % (length * 2)) % (pos, name, value))
  200. def dumpVia(data, base, layers, cols, rows, macros,
  201. layout_options_size, custom_config_size):
  202. magicYear = data[base + 0]
  203. magicMonth = data[base + 1]
  204. magicDay = data[base + 2]
  205. # Sanity check
  206. if not 10 <= magicYear <= 0x99 or \
  207. not 0 <= magicMonth <= 0x12 or \
  208. not 0 <= magicDay <= 0x31:
  209. print("ERROR: VIA Signature is not valid; Year:%x, Month:%x, Day:%x" % (magicYear, magicMonth, magicDay))
  210. return
  211. if cols is None or rows is None:
  212. print("ERROR: VIA dump requires specifying --rows and --cols", file=sys.stderr)
  213. return 2
  214. print("VIA:")
  215. # Decode magic
  216. print("%04x MAGIC = 20%02x-%02x-%02x" % (base, magicYear, magicMonth, magicDay))
  217. # Decode layout options
  218. options = 0
  219. pos = base + 3
  220. for i in range(base+3, base+3+layout_options_size):
  221. options = options << 8
  222. options |= data[i]
  223. print(("%%04x LAYOUT_OPTIONS = 0x%%0%dx" % (layout_options_size * 2)) % (pos, options))
  224. pos += layout_options_size + custom_config_size
  225. # Decode keycodes
  226. keymap_size = layers * rows * cols * 2
  227. if (pos + keymap_size) >= (len(data) - 1):
  228. print("ERROR: VIA keymap requires %d bytes, but only %d available" % (keymap_size, len(data) - pos))
  229. return 3
  230. for layer in range(layers):
  231. print("%s LAYER %d %s" % ('-'*int(cols*2.5), layer, '-'*int(cols*2.5)))
  232. for row in range(rows):
  233. print("%04x | " % pos, end='')
  234. for col in range(cols):
  235. keycode = (data[pos] << 8) | (data[pos+1])
  236. print(" %04x" % keycode, end='')
  237. pos += 2
  238. print("")
  239. # Decode macros
  240. for macro_num in range(macros):
  241. macro = ""
  242. macro_pos = pos
  243. while pos < len(data):
  244. char = chr(data[pos])
  245. pos += 1
  246. if char == '\x00':
  247. print("%04x MACRO[%d] = '%s'" % (macro_pos, macro_num, macro))
  248. break
  249. else:
  250. macro += char
  251. return 0
  252. def decodeSTM32Eeprom(input, canonical, size=None, output=None, **kwargs):
  253. input_size = os.path.getsize(input)
  254. if size is None:
  255. size = input_size >> 1
  256. # Read the first few bytes to check magic signature
  257. with open(input, 'rb') as in_file:
  258. magic=in_file.read(4)
  259. in_file.seek(0)
  260. if magic == MAGIC_FEEA:
  261. decoded = decodeEepromFEEA(in_file, size)
  262. eeconfig = EECONFIG_V1
  263. via_base = VIABASE_V1
  264. elif magic[:2] == MAGIC_FEE9:
  265. decoded = decodeEepromFEE9(in_file, size)
  266. eeconfig = EECONFIG_V1
  267. via_base = VIABASE_V1
  268. else:
  269. print("Unknown magic signature: %s" % " ".join(["0x%02x" % ord(x) for x in magic]), file=sys.stderr)
  270. return 1
  271. if output is not None:
  272. with open(output, 'wb') as out_file:
  273. out_file.write(pack('%dB' % len(decoded), *decoded))
  274. print("DECODED EEPROM:")
  275. dumpBinary(decoded, canonical)
  276. dumpEeconfig(decoded, eeconfig)
  277. if kwargs['rows'] is not None and kwargs['cols'] is not None:
  278. return dumpVia(decoded, via_base, **kwargs)
  279. return 0
  280. def main():
  281. global VERBOSE
  282. kwargs = vars(parseArgs())
  283. VERBOSE = kwargs.pop('verbose')
  284. return decodeSTM32Eeprom(**kwargs)
  285. if __name__ == '__main__':
  286. sys.exit(main())