Coverage for hdl_registers/register_list.py: 100%

158 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-01-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/hdl_registers/hdl_registers 

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

9 

10# Standard libraries 

11import copy 

12import datetime 

13import hashlib 

14import re 

15from pathlib import Path 

16from shutil import copy2 

17 

18# Third party libraries 

19from tsfpga import DEFAULT_FILE_ENCODING 

20from tsfpga.git_utils import get_git_commit, git_commands_are_available 

21from tsfpga.svn_utils import get_svn_revision_information, svn_commands_are_available 

22from tsfpga.system_utils import create_directory, create_file, read_file 

23 

24# Local folder libraries 

25from . import __version__ 

26from .constant import Constant 

27from .register import Register 

28from .register_array import RegisterArray 

29from .register_c_generator import RegisterCGenerator 

30from .register_cpp_generator import RegisterCppGenerator 

31from .register_html_generator import RegisterHtmlGenerator 

32from .register_python_generator import RegisterPythonGenerator 

33from .register_vhdl_generator import RegisterVhdlGenerator 

34 

35 

36class RegisterList: 

37 

38 """ 

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

40 """ 

41 

42 def __init__(self, name, source_definition_file): 

43 """ 

44 Arguments: 

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

46 it. 

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

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

49 for traceability. 

50 """ 

51 self.name = name 

52 self.source_definition_file = source_definition_file 

53 

54 self.register_objects = [] 

55 self.constants = [] 

56 

57 @classmethod 

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

59 """ 

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

61 

62 Arguments: 

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

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

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

66 for traceability. 

67 

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

69 use case. 

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

71 register list. 

72 """ 

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

74 register_list.register_objects = copy.deepcopy(default_registers) 

75 return register_list 

76 

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

78 """ 

79 Append a register to this list. 

80 

81 Arguments: 

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

83 mode (str): A valid register mode. 

84 description (str): Textual register description. 

85 Return: 

86 Register: The register object that was created. 

87 """ 

88 if self.register_objects: 

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

90 else: 

91 index = 0 

92 

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

94 self.register_objects.append(register) 

95 

96 return register 

97 

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

99 """ 

100 Append a register array to this list. 

101 

102 Arguments: 

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

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

105 Return: 

106 RegisterArray: The register array object that was created. 

107 """ 

108 if self.register_objects: 

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

110 else: 

111 base_index = 0 

112 register_array = RegisterArray( 

113 name=name, base_index=base_index, length=length, description=description 

114 ) 

115 

116 self.register_objects.append(register_array) 

117 return register_array 

118 

119 def get_register(self, name): 

120 """ 

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

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

123 

124 Arguments: 

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

126 Return: 

127 Register: The register. 

128 """ 

129 for register_object in self.register_objects: 

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

131 return register_object 

132 

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

134 

135 def get_register_array(self, name): 

136 """ 

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

138 

139 Arguments: 

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

141 Return: 

142 RegisterArray: The register array. 

143 """ 

144 for register_object in self.register_objects: 

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

146 return register_object 

147 

148 raise ValueError( 

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

150 ) 

151 

152 def get_register_index( 

153 self, register_name, register_array_name=None, register_array_index=None 

154 ): 

155 """ 

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

157 

158 Arguments: 

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

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

161 of the array must be specified. 

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

163 iteration index must be specified. 

164 

165 Return: 

166 int: The index. 

167 """ 

168 if register_array_name is None and register_array_index is None: 

169 # Target is plain register 

170 register = self.get_register(register_name) 

171 

172 return register.index 

173 

174 # Target is in register array 

175 register_array = self.get_register_array(register_array_name) 

176 register_array_start_index = register_array.get_start_index(register_array_index) 

177 

178 register = register_array.get_register(register_name) 

179 register_index = register.index 

180 

181 return register_array_start_index + register_index 

182 

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

184 """ 

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

186 

187 Arguments: 

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

189 value (bool, int, str): The constant value. 

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

191 Return: 

192 Constant: The constant object that was created. 

193 """ 

194 constant = Constant(name=name, value=value, description=description) 

195 self.constants.append(constant) 

196 return constant 

197 

198 def get_constant(self, name): 

199 """ 

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

201 

202 Arguments: 

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

204 Return: 

205 Constant: The constant. 

206 """ 

207 for constant in self.constants: 

208 if constant.name == name: 

209 return constant 

210 

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

212 

213 def create_vhdl_package(self, output_path): 

214 """ 

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

216 

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

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

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

220 like the others (when making a release). 

221 

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

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

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

225 generated again if something has changed. 

226 

227 Arguments: 

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

229 """ 

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

231 

232 self_hash = self._hash() 

233 if self._should_create_vhdl_package(vhd_file, self_hash): 

234 self._create_vhdl_package(vhd_file, self_hash) 

235 

236 def _should_create_vhdl_package(self, vhd_file, self_hash): 

237 if not vhd_file.exists(): 

238 return True 

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

240 vhd_file 

241 ): 

242 return True 

243 return False 

244 

245 @staticmethod 

246 def _find_hash_and_version_of_existing_vhdl_package(vhd_file): 

247 """ 

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

249 """ 

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

251 existing_file_content = read_file(vhd_file) 

252 match = regexp.search(existing_file_content) 

253 if match is None: 

254 return None 

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

256 

257 def _create_vhdl_package(self, vhd_file, self_hash): 

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

259 # Add a header line with the hash 

260 generated_info = self.generated_source_info() + [ 

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

262 ] 

263 register_vhdl_generator = RegisterVhdlGenerator(self.name, generated_info) 

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

265 file_handle.write( 

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

267 ) 

268 

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

270 """ 

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

272 

273 Arguments: 

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

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

276 """ 

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

278 output_file = output_path / file_name 

279 

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

281 create_file( 

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

283 ) 

284 

285 def create_cpp_interface(self, output_path): 

286 """ 

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

288 interface header contains only virtual methods. 

289 

290 Arguments: 

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

292 """ 

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

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

295 create_file( 

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

297 ) 

298 

299 def create_cpp_header(self, output_path): 

300 """ 

301 Create a C++ class header file. 

302 

303 Arguments: 

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

305 """ 

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

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

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

309 

310 def create_cpp_implementation(self, output_path): 

311 """ 

312 Create a C++ class implementation file. 

313 

314 Arguments: 

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

316 """ 

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

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

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

320 

321 def create_html_page(self, output_path): 

322 """ 

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

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

325 :meth:`.create_html_constant_table`. 

326 

327 Arguments: 

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

329 """ 

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

331 

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

333 create_file( 

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

335 ) 

336 

337 stylesheet = register_html_generator.get_page_style() 

338 stylesheet_file = output_path / "regs_style.css" 

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

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

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

342 # HTML file? 

343 create_file(stylesheet_file, stylesheet) 

344 

345 def create_html_register_table(self, output_path): 

346 """ 

347 Create documentation HTML table with register and field information. 

348 

349 Arguments: 

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

351 """ 

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

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

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

355 

356 def create_html_constant_table(self, output_path): 

357 """ 

358 Create documentation HTML table with constant information. 

359 

360 Arguments: 

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

362 """ 

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

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

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

366 

367 def create_python_class(self, output_path): 

368 """ 

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

370 

371 Arguments: 

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

373 """ 

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

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

376 

377 def copy_source_definition(self, output_path): 

378 """ 

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

380 be copied. 

381 

382 Arguments: 

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

384 """ 

385 if self.source_definition_file is not None: 

386 create_directory(output_path, empty=False) 

387 copy2(self.source_definition_file, output_path) 

388 

389 @staticmethod 

390 def generated_info(): 

391 """ 

392 Return: 

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

394 """ 

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

396 

397 def generated_source_info(self): 

398 """ 

399 Return: 

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

401 info about the source of the generated register information. 

402 """ 

403 # Default to the user's current working directory 

404 directory = Path(".") 

405 

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

407 

408 file_info = "" 

409 if self.source_definition_file is not None: 

410 directory = self.source_definition_file.parent 

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

412 

413 commit_info = "" 

414 if git_commands_are_available(directory): 

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

416 elif svn_commands_are_available(directory): 

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

418 

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

420 

421 return self.generated_info() + [info] 

422 

423 def _hash(self): 

424 """ 

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

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

427 Result is a lowercase hexadecimal string. 

428 """ 

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

430 

431 def __repr__(self): 

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

433name={self.name},\ 

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

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

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

437)"""