#
# MIT License
#
# Copyright (c) 2023 nbiotcloud
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
"""Hardware Related Numeric Calculations."""
import math
import typing
from .addrrange import AddrRange
[docs]class AlignError(RuntimeError):
"""Alignment Error."""
[docs]def unsigned_to_hex(value: int, width: int, prefix: str = "") -> str:
"""
Convert unsigned `value` to `width` bit hex.
Example:
>>> unsigned_to_hex(166, 8)
'A6'
>>> unsigned_to_hex(3, 6)
'03'
>>> unsigned_to_hex(5, 9)
'005'
>>> unsigned_to_hex(26000000, 32)
'018CBA80'
>>> unsigned_to_hex(16, 4)
Traceback (most recent call last):
...
ValueError: 16 is not a unsigned 4 bit integer
>>> unsigned_to_hex(-1, 4)
Traceback (most recent call last):
...
ValueError: -1 is not a unsigned 4 bit integer
>>> unsigned_to_hex(26000000, 32, prefix="{width}'h")
"32'h018CBA80"
>>> unsigned_to_hex(248, 8, prefix="0x")
'0xF8'
"""
low = 0
high = (1 << width) - 1
if value < low or value > high:
msg = f"{value} is not a unsigned {width} bit integer"
raise ValueError(msg)
hexwidth = (width + 3) >> 2
pfx = prefix.format(value=value, width=width)
return f"{pfx}{value:0{hexwidth}X}"
[docs]def signed_to_hex(value: int, width: int, prefix: str = "") -> str:
"""
Convert signed `value` to `width` bit hex.
Example:
>>> signed_to_hex(15, 8)
'0F'
>>> signed_to_hex(-3, 6)
'3D'
>>> signed_to_hex(5, 9)
'005'
>>> signed_to_hex(-9, 4)
Traceback (most recent call last):
...
ValueError: -9 is not a signed 4 bit integer
>>> signed_to_hex(8, 4)
Traceback (most recent call last):
...
ValueError: 8 is not a signed 4 bit integer
>>> signed_to_hex(5, 9, prefix="{width}'h")
"9'h005"
"""
hexwidth = (width + 3) >> 2
value = signed_to_unsigned(value, width)
pfx = prefix.format(value=value, width=width)
return f"{pfx}{value:0{hexwidth}X}"
[docs]def signed_to_unsigned(value: int, width: int) -> int:
"""
Convert signed `value` to unsigned with `width`.
Example:
>>> signed_to_unsigned(3, 4)
3
>>> signed_to_unsigned(-3, 4)
13
>>> signed_to_unsigned(-3, 8)
253
>>> signed_to_unsigned(-9, 4)
Traceback (most recent call last):
...
ValueError: -9 is not a signed 4 bit integer
>>> signed_to_unsigned(8, 4)
Traceback (most recent call last):
...
ValueError: 8 is not a signed 4 bit integer
"""
high = (1 << (width - 1)) - 1
low = ~high
if value < low or value > high:
msg = f"{value} is not a signed {width} bit integer"
raise ValueError(msg)
return (value + (1 << width)) & (~(-1 << width))
[docs]def unsigned_to_signed(value: int, width: int) -> int:
"""
Convert unsigned `value` to signed with `width`.
Example:
>>> unsigned_to_signed(3, 4)
3
>>> unsigned_to_signed(13, 4)
-3
>>> unsigned_to_signed(253, 8)
-3
>>> unsigned_to_signed(-9, 4)
Traceback (most recent call last):
...
ValueError: -9 is not a unsigned 4 bit integer
"""
low = 0
high = (1 << width) - 1
if value < low or value > high:
msg = f"{value} is not a unsigned {width} bit integer"
raise ValueError(msg)
if value & (1 << (width - 1)): # MSB set->negative
return value - (1 << width)
return value
[docs]def calc_unsigned_width(value: int) -> int:
"""
Return width in bits for `value`.
>>> calc_unsigned_width(3)
2
>>> calc_unsigned_width(4)
3
>>> calc_unsigned_width(7)
3
>>> calc_unsigned_width(8)
4
>>> calc_unsigned_width(15)
4
>>> calc_unsigned_width(16)
5
>>> calc_unsigned_width(8191)
13
>>> calc_unsigned_width(0)
1
>>> calc_unsigned_width(-5)
Traceback (most recent call last):
...
AssertionError: Value must be not negative. -5 is not.
"""
assert value >= 0, f"Value must be not negative. {value!r} is not."
return (value | 1).bit_length() # to handle the case value==0
[docs]def calc_signed_width(value: int) -> int:
"""
Return width in bits for `value`.
>>> calc_signed_width(15)
5
>>> calc_signed_width(16)
6
>>> calc_signed_width(8191)
14
>>> calc_signed_width(-7)
4
>>> calc_signed_width(-8)
4
>>> calc_signed_width(-9)
5
>>> calc_signed_width(0)
1
"""
if value < 0:
value = ~value
return value.bit_length() + 1
[docs]def calc_lowest_bit_set(num: int) -> typing.Optional[int]:
"""
Return bit number which is not zero.
Example:
>>> calc_lowest_bit_set(0xE0)
5
>>> calc_lowest_bit_set(-8)
3
>>> calc_lowest_bit_set(0)
"""
if num:
bit = 0
# pylint: disable=superfluous-parens
while not (num & 0x1):
num >>= 1
bit += 1
return bit
return None
[docs]def is_power_of2(value: int) -> int:
"""
Return `True` if `value` is power of 2.
>>> is_power_of2(15)
False
>>> is_power_of2(16)
True
>>> is_power_of2(17)
False
>>> is_power_of2(-5)
Traceback (most recent call last):
...
AssertionError: Value must be larger than zero. -5 is not.
>>> is_power_of2(0)
False
"""
assert value >= 0, f"Value must be larger than zero. {value!r} is not."
return value > 0 and ((value & (value - 1)) == 0)
[docs]def is_power_of(value: int, base: int = 2) -> int:
"""
Return `True` if `value` is power of `base`.
>>> is_power_of(15)
False
>>> is_power_of(16)
True
>>> is_power_of(17)
False
>>> is_power_of(-5)
Traceback (most recent call last):
...
AssertionError: Value must be larger than zero. -5 is not.
>>> is_power_of(8, base=3)
False
>>> is_power_of(9, base=3)
True
>>> is_power_of(-9, base=3)
Traceback (most recent call last):
...
AssertionError: Value must be larger than zero. -9 is not.
>>> is_power_of(0)
False
>>> is_power_of(0, base=6)
False
"""
assert value >= 0, f"Value must be larger than zero. {value!r} is not."
if value == 0:
return False
if base == 2:
return (value & (value - 1)) == 0
exp = math.log(value, base)
return exp == int(exp)
[docs]def calc_next_power_of2(value: int):
"""
Return next power of 2.
The returned value fulfills the rule `2**ceil(log2(value)) >= value`.
>>> calc_next_power_of2(1)
2
>>> calc_next_power_of2(10)
16
>>> calc_next_power_of2(16)
16
>>> calc_next_power_of2(17)
32
>>> calc_next_power_of2(-5)
Traceback (most recent call last):
...
AssertionError: Value must be positive. -5 is not.
"""
assert value > 0, f"Value must be positive. {value!r} is not."
return 1 << calc_unsigned_width(value - 1)
[docs]def calc_next_power_of(value: int, base: int = 2):
"""
Return next power of `base`.
The returned value fulfills the rule `base**ceil(log_base(value)) >= value`.
>>> calc_next_power_of(1)
2
>>> calc_next_power_of(1, base=5)
5
>>> calc_next_power_of(10)
16
>>> calc_next_power_of(10, base=3)
27
>>> calc_next_power_of(16)
16
>>> calc_next_power_of(9, base=3)
9
>>> calc_next_power_of(17)
32
>>> calc_next_power_of(-5)
Traceback (most recent call last):
...
AssertionError: Value must be positive. -5 is not.
"""
assert value > 0, f"Value must be positive. {value!r} is not."
exp = max(math.ceil(math.log(value, base)), 1)
return base**exp
[docs]def calc_prev_power_of2(value: int):
"""
Return previous power of `base`.
The returned value fulfills the rule `2**floor(log2(value)) <= value`.
>>> calc_prev_power_of2(1)
1
>>> calc_prev_power_of2(15)
8
>>> calc_prev_power_of2(16)
16
>>> calc_prev_power_of2(17)
16
>>> calc_prev_power_of2(-5)
Traceback (most recent call last):
...
AssertionError: Value must be positive. -5 is not.
"""
assert value > 0, f"Value must be positive. {value!r} is not."
return 1 << (calc_unsigned_width(value) - 1)
[docs]def calc_prev_power_of(value: int, base: int = 2):
"""
Return previous power of `base`.
The returned value fulfills the rule `base**exp <= value`.
>>> calc_prev_power_of(1)
1
>>> calc_prev_power_of(1, base=7)
1
>>> calc_prev_power_of(15)
8
>>> calc_prev_power_of(16)
16
>>> calc_prev_power_of(17)
16
>>> calc_prev_power_of(10, base=3)
9
>>> calc_prev_power_of(27, base=3)
27
>>> calc_prev_power_of(-5)
Traceback (most recent call last):
...
AssertionError: Value must be positive. -5 is not.
"""
assert value > 0, f"Value must be positive. {value!r} is not."
exp = math.floor(math.log(value, base))
return base**exp
# pylint: disable=redefined-outer-name
[docs]def align(
value: int,
offset: typing.Optional[int] = None,
align: typing.Optional[int] = None,
minalign: int = 1,
rewind=False,
):
"""
Forward `value` to `offset` and `align` and `minalign`.
Without `offset` and `align` nothing happens.
>>> align(5)
5
>>> align(7)
7
An `offset` forwards the count if necessary or raises an :any:`AlignError`.
>>> align(5, offset=8)
8
>>> align(5, offset=3)
Traceback (most recent call last):
...
icdutil.num.AlignError: Cannot use offset 3 as we are already at 5
A `align` forwards the value to the next multiple of `align`.
>>> align(5, align=4)
8
>>> align(8, align=4)
8
>>> align(9, align=4)
12
A `minalign` without `align` forwards the value to the next multiple of `minalign`
>>> align(5, minalign=4)
8
>>> align(8, minalign=4)
8
>>> align(9, minalign=4)
12
If both `align` and `minalign` are given, then the value is moved to the next multiple
of whichever of the both align values is bigger
>>> align(8, align=5, minalign=4)
10
>>> align(8, align=4, minalign=6)
12
If `offset` is given it is dominant over both `align` and `minalign`
>>> align(8, offset=9, align=4, minalign=6)
9
"""
if offset is not None:
if not rewind and value > offset:
raise AlignError(f"Cannot use offset {offset} as we are already at {value}")
return offset
curalign = max(align, minalign) if align is not None else minalign
misalign = value % curalign
if misalign:
value += curalign - misalign
return value
[docs]def bytes2words(bytes_: typing.Sequence[int], bytesperword=4) -> typing.Sequence[int]:
"""
Convert list of bytes to list of words.
>>> for word in bytes2words([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]):
... print("0x%X" % word)
0x44332211
0x6655
>>> for word in bytes2words(bytes.fromhex("112233445566")):
... print("0x%X" % word)
0x44332211
0x6655
>>> for word in bytes2words([0x11, 0x22, 0x33, 0x44, 0x55, 0x66], bytesperword=2):
... print("0x%X" % word)
0x2211
0x4433
0x6655
>>> for word in bytes2words([]):
... print("0x%X" % word)
"""
words = []
word = i = 0
for byte_ in bytes_:
word += byte_ << (8 * i)
i += 1
if i == bytesperword:
words.append(word)
word = i = 0
if i != 0:
words.append(word)
return words
[docs]def bytes2word(bytes_: typing.Sequence[int]) -> int:
"""
Convert list of bytes to one word.
>>> print("0x%X" % bytes2word([0x11, 0x22, 0x33, 0x44, 0x55, 0x66]))
0x665544332211
"""
word = 0
for i, byte_ in enumerate(bytes_):
word += byte_ << (8 * i)
return word
[docs]def words2bytes(words: typing.Sequence[int], bytesperword: int = 4) -> typing.Sequence[int]:
"""
Convert list of words to list of bytes.
>>> for byte_ in words2bytes([0x44332211, 0x6655]):
... print("0x%X" % byte_)
0x11
0x22
0x33
0x44
0x55
0x66
0x0
0x0
>>> for byte_ in words2bytes([0x44332211, 0x6655], bytesperword=6):
... print("0x%X" % byte_)
0x11
0x22
0x33
0x44
0x0
0x0
0x55
0x66
0x0
0x0
0x0
0x0
"""
bytes_ = []
for word in words:
for _ in range(bytesperword):
byte_ = word & 0xFF
word >>= 8
bytes_.append(byte_)
return bytes_
[docs]def convwidth(iterable, srcwidth=32, destwidth=8):
"""
Convert iterable with values of `srcwidth` to list of values of `destwidth`.
>>> [hex(i) for i in convwidth([0x04030201, 0x08070605], 32, 16)]
['0x201', '0x403', '0x605', '0x807']
>>> [hex(i) for i in convwidth([0x0807060504030201], 64, 16)]
['0x201', '0x403', '0x605', '0x807']
>>> [hex(i) for i in convwidth([0x0807060504030201], 64, 8)]
['0x1', '0x2', '0x3', '0x4', '0x5', '0x6', '0x7', '0x8']
>>> [hex(i) for i in convwidth([], 32, 16)]
[]
>>> [hex(i) for i in convwidth([0x201, 0x403, 0x605, 0x807], 16, 32)]
['0x4030201', '0x8070605']
>>> [hex(i) for i in convwidth([0x201, 0x403, 0x605, 0x807], 16, 64)]
['0x807060504030201']
>>> [hex(i) for i in convwidth([0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8], 8, 64)]
['0x807060504030201']
>>> [hex(i) for i in convwidth([0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8], 8, 56)]
['0x7060504030201', '0x8']
>>> [hex(i) for i in convwidth([], 16, 32)]
[]
>>> [hex(i) for i in convwidth([0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8], 8, 8)]
['0x1', '0x2', '0x3', '0x4', '0x5', '0x6', '0x7', '0x8']
"""
if srcwidth > destwidth:
# wider to smaller
assert (srcwidth % destwidth) == 0
mask = (1 << destwidth) - 1
iterations = srcwidth // destwidth
for item in iterable:
for _ in range(iterations):
yield item & mask
item >>= destwidth
assert item == 0
elif srcwidth < destwidth:
# smaller to wider
assert (destwidth % srcwidth) == 0
dest = i = 0
for item in iterable:
dest += item << i
i += srcwidth
if i == destwidth:
yield dest
dest = i = 0
if i != 0:
yield dest
else:
yield from iterable
[docs]def to_mask(addrwidth: int, baseaddr: int, exp: int, dontcare="?") -> str:
"""
Convert address range starting at `baseaddr` with 2**`exp` elements to mask with `addrwidth`
using the `dontcare` character as mask bits.
>>> to_mask(16, 0x4000, 6)
'0100000000??????'
>>> to_mask(16, 0x6000, 4, 'x')
'011000000000xxxx'
>>> to_mask(16, 0x4004, 6)
Traceback (most recent call last):
...
AssertionError: 6 LSBs of '0100000000000100' shall be '000000', not '000100'
"""
base = bin(baseaddr)[2:].zfill(addrwidth)
if exp:
zeros = "0" * exp
lsbs = base[-exp:]
assert lsbs == zeros, f"{exp} LSBs of {base!r} shall be {zeros!r}, not {lsbs!r}"
if exp:
return base[:-exp] + dontcare * exp
return base
def _iter_powerof2_segs(baseaddr: int, size: int) -> typing.Generator[typing.Tuple[int, int], None, None]:
"""
Split region between at `baseaddr` and `baseaddr+size-1` into segments with maximum powers of 2.
>>> for segm, bits in _iter_powerof2_segs(0x53, 0x80):
... print(f"{hex(segm)}, {1<<bits}")
0x53, 1
0x54, 4
0x58, 8
0x60, 32
0x80, 64
0xc0, 16
0xd0, 2
0xd2, 1
>>> for segm, bits in _iter_powerof2_segs(0x100, 0x80):
... print(f"{hex(segm)}, {1<<bits}")
0x100, 128
>>> for segm, bits in _iter_powerof2_segs(0x140, 0x80):
... print(f"{hex(segm)}, {1<<bits}")
0x140, 64
0x180, 64
>>> for segm, bits in _iter_powerof2_segs(0x0, 0x27):
... print(f"{hex(segm)}, {1<<bits}")
0x0, 32
0x20, 4
0x24, 2
0x26, 1
"""
endaddr = baseaddr + size - 1
# search largest chunk fitting aligned into window of size
bits = 0
for bits in reversed(range(size.bit_length())):
csize = 1 << bits
cbase = align(baseaddr, align=csize)
cend = cbase + csize - 1
if bits and cend <= endaddr:
break
# before chunk
if baseaddr < cbase:
yield from _iter_powerof2_segs(baseaddr, (cbase - baseaddr))
# chunk
yield cbase, bits
# after chunk
if cend < endaddr:
yield from _iter_powerof2_segs(cend + 1, (endaddr - cend))
[docs]def calc_addrwinmasks(baseaddr, size, addrwidth=32, dontcare="?") -> typing.Tuple[str, ...]:
"""
Return tuple of binary compare wildcard masks for address window of `size` starting at `baseaddr`.
Args:
baseaddr (int): Address window start addresss
size (int): Address window size
Keyword Args:
addrwidth: Address width in bits
dontcare: Character used for don't care bits
Example:
>>> calc_addrwinmasks(0, 1, addrwidth=4)
('0000',)
>>> calc_addrwinmasks(0xF000, 0x10, addrwidth=16)
('111100000000????',)
>>> calc_addrwinmasks(0xF000, 0x180, addrwidth=16, dontcare='x')
('11110000xxxxxxxx', '111100010xxxxxxx')
>>> calc_addrwinmasks(0xEFF0, 0x100, addrwidth=16)
('111011111111????', '111100000???????', '1111000010??????', '11110000110?????', '111100001110????')
"""
assert len(dontcare) == 1, f"dontcare '{dontcare}' shall have length of 1"
return tuple(to_mask(addrwidth, base, exp, dontcare) for base, exp in _iter_powerof2_segs(baseaddr, size))
def _iter_aligned_segs(baseaddr: int, size: int) -> typing.Generator[AddrRange, None, None]:
endaddr = baseaddr + size - 1
# search largest chunk fitting aligned into window of size
sizeup = calc_next_power_of2(size)
baseup = align(baseaddr, align=sizeup)
end = baseup + size - 1
while end > endaddr:
sizeup //= 2
size = sizeup
baseup = align(baseaddr, align=sizeup)
end = baseup + size - 1
assert baseaddr <= baseup
# before chunk
pre_size = baseup - baseaddr
if pre_size > 0:
yield from _iter_aligned_segs(baseaddr, pre_size)
# chunk
yield AddrRange(baseup, size)
# after chunk
post_size = endaddr - end
if post_size > 0:
yield from _iter_aligned_segs(end + 1, post_size)
[docs]def split_aligned_segs(baseaddr: int, size: int) -> typing.Tuple[AddrRange, ...]:
"""
Split address window starting at `baseaddr` with `size` into segments with aligned base addresses.
The base addresses of the segments are aligned to `calc_next_power_of2(size)`.
Returns tuple of :any:`AddrRange`. Segment starting at `baseaddr` with `size`.
Args:
baseaddr (int): Address window start addresss
size (int): Address window size
Example:
>>> split_aligned_segs(0, 1)
(AddrRange(0x0, '1 byte'),)
>>> split_aligned_segs(1024, 16)
(AddrRange(0x400, '16 bytes'),)
>>> split_aligned_segs(1024, 1024)
(AddrRange(0x400, '1 KB'),)
>>> split_aligned_segs(1024, 1024+768)
(AddrRange(0x400, '1 KB'), AddrRange(0x800, '768 bytes'))
>>> split_aligned_segs(256, 1024+768)
(AddrRange(0x100, '256 bytes'), AddrRange(0x200, '512 bytes'), AddrRange(0x400, '1 KB'))
>>> split_aligned_segs(1000, 1024) # doctest: +ELLIPSIS
(AddrRange(0x3E8, '8 bytes'), AddrRange(0x3F0, '16 bytes'), AddrRange(0x400, '512 bytes'), ...(0x600, '488 bytes'))
"""
return tuple(_iter_aligned_segs(baseaddr, size))