Coverage for hdl_registers/register_list.py: 100%

158 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2022-09-29 22:03 +0000

1# -------------------------------------------------------------------------------------------------- 

2# Copyright (c) Lukas Vik. All rights reserved. 

3# 

4# This file is part of the hdl_registers project, a HDL register generator fast enough to be run 

5# in real time. 

6# https://hdl-registers.com 

7# https://gitlab.com/tsfpga/hdl_registers 

8# -------------------------------------------------------------------------------------------------- 

9 

10import copy 

11import hashlib 

12import datetime 

13import re 

14from shutil import copy2 

15 

16from pathlib import Path 

17 

18from tsfpga import DEFAULT_FILE_ENCODING 

19from tsfpga.git_utils import git_commands_are_available, get_git_commit 

20from tsfpga.svn_utils import svn_commands_are_available, get_svn_revision_information 

21from tsfpga.system_utils import create_directory, create_file, read_file 

22 

23from . import __version__ 

24from .constant import Constant 

25from .register import Register 

26from .register_array import RegisterArray 

27from .register_c_generator import RegisterCGenerator 

28from .register_cpp_generator import RegisterCppGenerator 

29from .register_html_generator import RegisterHtmlGenerator 

30from .register_python_generator import RegisterPythonGenerator 

31from .register_vhdl_generator import RegisterVhdlGenerator 

32 

33 

34class RegisterList: 

35 

36 """ 

37 Used to handle the registers of a module. Also known as a register map. 

38 """ 

39 

40 def __init__(self, name, source_definition_file): 

41 """ 

42 Arguments: 

43 name (str): The name of this register list. Typically the name of the module that uses 

44 it. 

45 source_definition_file (pathlib.Path): The TOML source file that defined this 

46 register list. Will be displayed in generated source code and documentation 

47 for traceability. 

48 """ 

49 self.name = name 

50 self.source_definition_file = source_definition_file 

51 

52 self.register_objects = [] 

53 self.constants = [] 

54 

55 @classmethod 

56 def from_default_registers(cls, name, source_definition_file, default_registers): 

57 """ 

58 Factory method. Create a RegisterList object from a plain list of registers. 

59 

60 Arguments: 

61 name (str): The name of this register list. 

62 source_definition_file (pathlib.Path): The source file that defined this 

63 register list. Will be displayed in generated source code and documentation 

64 for traceability. 

65 

66 Can be set to ``None`` if this information does not make sense in the current 

67 use case. 

68 default_registers (list(Register)): These registers will be inserted in the 

69 register list. 

70 """ 

71 register_list = cls(name=name, source_definition_file=source_definition_file) 

72 register_list.register_objects = copy.deepcopy(default_registers) 

73 return register_list 

74 

75 def append_register(self, name, mode, description): 

76 """ 

77 Append a register to this list. 

78 

79 Arguments: 

80 name (str): The name of the register. 

81 mode (str): A valid register mode. 

82 description (str): Textual register description. 

83 Return: 

84 Register: The register object that was created. 

85 """ 

86 if self.register_objects: 

87 index = self.register_objects[-1].index + 1 

88 else: 

89 index = 0 

90 

91 register = Register(name, index, mode, description) 

92 self.register_objects.append(register) 

93 

94 return register 

95 

96 def append_register_array(self, name, length, description): 

97 """ 

98 Append a register array to this list. 

99 

100 Arguments: 

101 name (str): The name of the register array. 

102 length (int): The number of times the register sequence shall be repeated. 

103 Return: 

104 RegisterArray: The register array object that was created. 

105 """ 

106 if self.register_objects: 

107 base_index = self.register_objects[-1].index + 1 

108 else: 

109 base_index = 0 

110 register_array = RegisterArray( 

111 name=name, base_index=base_index, length=length, description=description 

112 ) 

113 

114 self.register_objects.append(register_array) 

115 return register_array 

116 

117 def get_register(self, name): 

118 """ 

119 Get a register from this list. Will only find single registers, not registers in a 

120 register array. Will raise exception if no register matches. 

121 

122 Arguments: 

123 name (str): The name of the register. 

124 Return: 

125 Register: The register. 

126 """ 

127 for register_object in self.register_objects: 

128 if isinstance(register_object, Register) and register_object.name == name: 

129 return register_object 

130 

131 raise ValueError(f'Could not find register "{name}" within register list "{self.name}"') 

132 

133 def get_register_array(self, name): 

134 """ 

135 Get a register array from this list. Will raise exception if no register array matches. 

136 

137 Arguments: 

138 name (str): The name of the register array. 

139 Return: 

140 RegisterArray: The register array. 

141 """ 

142 for register_object in self.register_objects: 

143 if isinstance(register_object, RegisterArray) and register_object.name == name: 

144 return register_object 

145 

146 raise ValueError( 

147 f'Could not find register array "{name}" within register list "{self.name}"' 

148 ) 

149 

150 def get_register_index( 

151 self, register_name, register_array_name=None, register_array_index=None 

152 ): 

153 """ 

154 Get the zero-based index within the register list for the specified register. 

155 

156 Arguments: 

157 register_name (str): The name of the register. 

158 register_array_name (str): If the register is within a register array the name 

159 of the array must be specified. 

160 register_array_name (str): If the register is within a register array the array 

161 iteration index must be specified. 

162 

163 Return: 

164 int: The index. 

165 """ 

166 if register_array_name is None and register_array_index is None: 

167 # Target is plain register 

168 register = self.get_register(register_name) 

169 

170 return register.index 

171 

172 # Target is in register array 

173 register_array = self.get_register_array(register_array_name) 

174 register_array_start_index = register_array.get_start_index(register_array_index) 

175 

176 register = register_array.get_register(register_name) 

177 register_index = register.index 

178 

179 return register_array_start_index + register_index 

180 

181 def add_constant(self, name, value, description=None): 

182 """ 

183 Add a constant. Will be available in the generated packages and headers. 

184 

185 Arguments: 

186 name (str): The name of the constant. 

187 length (int): The constant value (signed). 

188 description (str): Textual description for the constant. 

189 Return: 

190 Constant: The constant object that was created. 

191 """ 

192 constant = Constant(name, value, description) 

193 self.constants.append(constant) 

194 return constant 

195 

196 def get_constant(self, name): 

197 """ 

198 Get a constant from this list. Will raise exception if no constant matches. 

199 

200 Arguments: 

201 name (str): The name of the constant. 

202 Return: 

203 Constant: The constant. 

204 """ 

205 for constant in self.constants: 

206 if constant.name == name: 

207 return constant 

208 

209 raise ValueError(f'Could not find constant "{name}" within register list "{self.name}"') 

210 

211 def create_vhdl_package(self, output_path): 

212 """ 

213 Create a VHDL package file with register and field definitions. 

214 

215 This function assumes that the ``output_path`` folder already exists. This assumption makes 

216 it slightly faster than the other functions that use ``create_file()``. Necessary since this 

217 one is often used in real time (before simulations, etc..) and not in one-off scenarios 

218 like the others (when making a release). 

219 

220 In order to save time, there is a mechanism to only generate the VHDL file when necessary. 

221 A hash of this register list object will be written to the file along with all the register 

222 definitions. This hash will be inspected and compared, and the VHDL file will only be 

223 generated again if something has changed. 

224 

225 Arguments: 

226 output_path (pathlib.Path): Result will be placed here. 

227 """ 

228 vhd_file = output_path / (self.name + "_regs_pkg.vhd") 

229 

230 self_hash = self._hash() 

231 if self._should_create_vhdl_package(vhd_file, self_hash): 

232 self._create_vhdl_package(vhd_file, self_hash) 

233 

234 def _should_create_vhdl_package(self, vhd_file, self_hash): 

235 if not vhd_file.exists(): 

236 return True 

237 if (self_hash, __version__) != self._find_hash_and_version_of_existing_vhdl_package( 

238 vhd_file 

239 ): 

240 return True 

241 return False 

242 

243 @staticmethod 

244 def _find_hash_and_version_of_existing_vhdl_package(vhd_file): 

245 """ 

246 Returns `None` if nothing found, otherwise the matching strings in a tuple. 

247 """ 

248 regexp = re.compile(r"\n-- Register hash ([0-9a-f]+), generator version (\S+)\.\n") 

249 existing_file_content = read_file(vhd_file) 

250 match = regexp.search(existing_file_content) 

251 if match is None: 

252 return None 

253 return match.group(1), match.group(2) 

254 

255 def _create_vhdl_package(self, vhd_file, self_hash): 

256 print(f"Creating VHDL register package {vhd_file}") 

257 # Add a header line with the hash 

258 generated_info = self.generated_source_info() + [ 

259 f"Register hash {self_hash}, generator version {__version__}." 

260 ] 

261 register_vhdl_generator = RegisterVhdlGenerator(self.name, generated_info) 

262 with open(vhd_file, "w", encoding=DEFAULT_FILE_ENCODING) as file_handle: 

263 file_handle.write( 

264 register_vhdl_generator.get_package(self.register_objects, self.constants) 

265 ) 

266 

267 def create_c_header(self, output_path, file_name=None): 

268 """ 

269 Create a C header file with register and field definitions. 

270 

271 Arguments: 

272 output_path (pathlib.Path): Result will be placed here. 

273 file_name (str): Optionally specify an explicit file name. 

274 """ 

275 file_name = f"{self.name}_regs.h" if file_name is None else file_name 

276 output_file = output_path / file_name 

277 

278 register_c_generator = RegisterCGenerator(self.name, self.generated_source_info()) 

279 create_file( 

280 output_file, register_c_generator.get_header(self.register_objects, self.constants) 

281 ) 

282 

283 def create_cpp_interface(self, output_path): 

284 """ 

285 Create a C++ class interface header file, with register and field definitions. The 

286 interface header contains only virtual methods. 

287 

288 Arguments: 

289 output_path (pathlib.Path): Result will be placed here. 

290 """ 

291 output_file = output_path / ("i_" + self.name + ".h") 

292 register_cpp_generator = RegisterCppGenerator(self.name, self.generated_source_info()) 

293 create_file( 

294 output_file, register_cpp_generator.get_interface(self.register_objects, self.constants) 

295 ) 

296 

297 def create_cpp_header(self, output_path): 

298 """ 

299 Create a C++ class header file. 

300 

301 Arguments: 

302 output_path (pathlib.Path): Result will be placed here. 

303 """ 

304 output_file = output_path / (self.name + ".h") 

305 register_cpp_generator = RegisterCppGenerator(self.name, self.generated_source_info()) 

306 create_file(output_file, register_cpp_generator.get_header(self.register_objects)) 

307 

308 def create_cpp_implementation(self, output_path): 

309 """ 

310 Create a C++ class implementation file. 

311 

312 Arguments: 

313 output_path (pathlib.Path): Result will be placed here. 

314 """ 

315 output_file = output_path / (self.name + ".cpp") 

316 register_cpp_generator = RegisterCppGenerator(self.name, self.generated_source_info()) 

317 create_file(output_file, register_cpp_generator.get_implementation(self.register_objects)) 

318 

319 def create_html_page(self, output_path): 

320 """ 

321 Create a documentation HTML page with register and field information. Will include the 

322 tables created by :meth:`.create_html_register_table` and 

323 :meth:`.create_html_constant_table`. 

324 

325 Arguments: 

326 output_path (pathlib.Path): Result will be placed here. 

327 """ 

328 register_html_generator = RegisterHtmlGenerator(self.name, self.generated_source_info()) 

329 

330 html_file = output_path / (self.name + "_regs.html") 

331 create_file( 

332 html_file, register_html_generator.get_page(self.register_objects, self.constants) 

333 ) 

334 

335 stylesheet = register_html_generator.get_page_style() 

336 stylesheet_file = output_path / "regs_style.css" 

337 if (not stylesheet_file.exists()) or read_file(stylesheet_file) != stylesheet: 

338 # Create the file only once. This mechanism could be made more smart, but at the moment 

339 # there is no use case. Perhaps there should be a separate stylesheet for each 

340 # HTML file? 

341 create_file(stylesheet_file, stylesheet) 

342 

343 def create_html_register_table(self, output_path): 

344 """ 

345 Create documentation HTML table with register and field information. 

346 

347 Arguments: 

348 output_path (pathlib.Path): Result will be placed here. 

349 """ 

350 output_file = output_path / (self.name + "_register_table.html") 

351 register_html_generator = RegisterHtmlGenerator(self.name, self.generated_source_info()) 

352 create_file(output_file, register_html_generator.get_register_table(self.register_objects)) 

353 

354 def create_html_constant_table(self, output_path): 

355 """ 

356 Create documentation HTML table with constant information. 

357 

358 Arguments: 

359 output_path (pathlib.Path): Result will be placed here. 

360 """ 

361 output_file = output_path / (self.name + "_constant_table.html") 

362 register_html_generator = RegisterHtmlGenerator(self.name, self.generated_source_info()) 

363 create_file(output_file, register_html_generator.get_constant_table(self.constants)) 

364 

365 def create_python_class(self, output_path): 

366 """ 

367 Save a python class with all register and constant information. 

368 

369 Arguments: 

370 output_path (pathlib.Path): Result will be placed here. 

371 """ 

372 register_python_generator = RegisterPythonGenerator(self.name, self.generated_source_info()) 

373 register_python_generator.create_class(register_list=self, output_folder=output_path) 

374 

375 def copy_source_definition(self, output_path): 

376 """ 

377 Copy the source file that created this register list. If no source file is set, nothing will 

378 be copied. 

379 

380 Arguments: 

381 output_path (pathlib.Path): Result will be placed here. 

382 """ 

383 if self.source_definition_file is not None: 

384 create_directory(output_path, empty=False) 

385 copy2(self.source_definition_file, output_path) 

386 

387 @staticmethod 

388 def generated_info(): 

389 """ 

390 Return: 

391 list(str): Line(s) informing the user that a file is automatically generated. 

392 """ 

393 return ["This file is automatically generated by hdl_registers."] 

394 

395 def generated_source_info(self): 

396 """ 

397 Return: 

398 list(str): Line(s) informing the user that a file is automatically generated, containing 

399 info about the source of the generated register information. 

400 """ 

401 # Default to the user's current working directory 

402 directory = Path(".") 

403 

404 time_info = datetime.datetime.now().strftime("%Y-%m-%d %H:%M") 

405 

406 file_info = "" 

407 if self.source_definition_file is not None: 

408 directory = self.source_definition_file.parent 

409 file_info = f" from file {self.source_definition_file.name}" 

410 

411 commit_info = "" 

412 if git_commands_are_available(directory): 

413 commit_info = f" at commit {get_git_commit(directory)}" 

414 elif svn_commands_are_available(directory): 

415 commit_info = f" at revision {get_svn_revision_information(directory)}" 

416 

417 info = f"Generated {time_info}{file_info}{commit_info}." 

418 

419 return self.generated_info() + [info] 

420 

421 def _hash(self): 

422 """ 

423 Get a hash of this object representation. SHA1 is the fastest method according to e.g. 

424 http://atodorov.org/blog/2013/02/05/performance-test-md5-sha1-sha256-sha512/ 

425 Result is a lowercase hexadecimal string. 

426 """ 

427 return hashlib.sha1(repr(self).encode()).hexdigest() 

428 

429 def __repr__(self): 

430 return f"""{self.__class__.__name__}(\ 

431name={self.name},\ 

432source_definition_file={repr(self.source_definition_file)},\ 

433register_objects={','.join([repr(register_object) for register_object in self.register_objects])},\ 

434constants={','.join([repr(constant) for constant in self.constants])},\ 

435)"""