aboutsummaryrefslogtreecommitdiff
path: root/mule-attiny/i2c-flash/src/mule-attiny_i2c_flash/i2c_flash.py
blob: 7beea78a444026c05ffd3b77d72922fb150c7d70 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
#! /usr/bin/env python3

# ATtiny816 I2C-Flashing script
# date: 15.05.2023

from smbus2 import SMBus, i2c_msg
from os import system
import sys
import gpiod
from time import sleep
from argparse import ArgumentParser
import pathlib
import logging
from abc import abstractmethod
from enum import IntEnum


# Bootloader I2C slave address
BL_I2C_DEV = 0x6f   # Reuse isl1208 address

# Error codes
FLASH_OK = 0
FLASH_FAIL = 1
INVALID_BIN = 2


def log_flash_error(msg, pos, bin_len):
    logging.info(f"Flashing at {pos}/{bin_len} B ...")
    logging.error(msg)


def load_bin(firmware_path):
    with open(firmware_path, "rb") as fd:
        return fd.read()


class BootModeController:

    def __init__(self, bootReqGpioChip, bootReqGpio, updiResetGpioChip, updiResetGpio):
        chip = gpiod.chip(bootReqGpioChip)
        self.__bootReq = chip.get_line(bootReqGpio)

        self.__updiResetGpioChip = updiResetGpioChip
        self.__updiResetGpio = updiResetGpio
        self.__flashMode = False

    def enterFlashloader(self):
        config = gpiod.line_request()
        config.consumer = "bootPin"
        config.request_type = gpiod.line_request.DIRECTION_OUTPUT

        self.__bootReq.request(config)
        self.__bootReq.set_value(1)
        self.__updi_reset()

        # Wait until ATtiny starts-up. Startup time (Power-ON <--dt--> Code-Exec) is fused to 64 ms (fuse 0x6).
        sleep(0.1)

        self.__flashMode = True
        self.enterRoMode()

    # Flashloader sub-mode (read-only: read bootloader data)
    def enterRoMode(self):
        if self.__flashMode:
            self.__bootReq.set_value(0)

    def exitRoMode(self):
        if self.__flashMode:
            self.__bootReq.set_value(1)

    def exitFlashloader(self):
        if self.__bootReq.is_requested():
            self.__bootReq.set_value(0)
            self.__bootReq.release()

        if (self.__flashMode):  # Don't leave ATtiny in flashloader mode when error occurs
            self.__updi_reset()

    def __updi_reset(self):
        # Issue the reset command
        # Give this executable the highest scheduling priority (-20) To avoid extra intra-packet sampling delays.
        ret = system(f"nice --20 updi_reset {self.__updiResetGpioChip} {self.__updiResetGpio}")

        if (ret != 0):
            raise Exception("UPDI Reset command failed!")


class BLHdrID(IntEnum):
    VERSION = 0
    # Add other bootloader header IDs here


def get_blhdr(blhdr):
    id = (blhdr & (0b111 << 5))
    if id == BLHdrID.VERSION:
        return BLVersionHdr(blhdr)
    # Add other bootloader header IDs here
    raise Exception(f"Unknown {id} ID for payload header")


class BLHdr():
    def __init__(self, hdr):
        self.hdr = hdr

    @abstractmethod
    def len(self):
        """Returns length of payload excluding header"""
        pass

    @abstractmethod
    def payload_class(self):
        """Returns the class to use for representing the data following the header"""
        pass


class BLVersionHdr(BLHdr):
    DIRTY_BIT_p = 4
    DIRTY_BIT_m = (0b1 << DIRTY_BIT_p)
    LAST_HASH_CHAR_p = 0
    LAST_HASH_CHAR_m = (0xf << LAST_HASH_CHAR_p)

    def len(self):
        """1B for MAJOR + 1B for MINOR + 1B for PATCH + 1B for NCOMMITS + 3B for 6 hex digits of the hash"""
        return 7

    def payload_class(self):
        return BLVersion

    def is_dirty(self):
        return bool(self.hdr & BLVersionHdr.DIRTY_BIT_m)

    def last_hash(self):
        return self.hdr & BLVersionHdr.LAST_HASH_CHAR_m


class BLVersion():
    def __init__(self, header, payload):
        self.major, self.minor, self.patch, self.ncommits, hash_12, hash_34, hash_56 = payload
        self.header = header
        self.dirty = self.header.is_dirty()
        self.hash = f"{hash_12:02x}{hash_34:02x}{hash_56:02x}{self.header.last_hash():0x}"

    def __str__(self):
        return f"Bootloader v{self.major}.{self.minor}.{self.patch}-{self.ncommits}-g{self.hash}{'-dirty' if self.dirty else ''} is online"  # noqa: E501


class BLParser():
    @classmethod
    def parse(cls, payload):
        if not payload:
            logging.error("No bootloader data is available!")
            return
        cursor = 0
        while cursor < len(payload):
            blhdr = get_blhdr(payload[cursor])
            clss = blhdr.payload_class()
            cursor += 1
            if len(payload) < cursor + blhdr.len():
                logging.error(f"Invalid data header of type {type(blhdr).__name__}")
                return
            yield clss(blhdr, payload[cursor:cursor + blhdr.len()])
            cursor += blhdr.len()


def crc16_xmodem(data):
    # Credits: https://gist.github.com/oysstu/68072c44c02879a2abf94ef350d1c7c6?permalink_comment_id=3943460#gistcomment-3943460    # noqa: E501
    '''
    CRC-16 (CCITT) implemented with a precomputed lookup table
    '''
    table = [
        0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50A5, 0x60C6, 0x70E7, 0x8108, 0x9129, 0xA14A,
        0xB16B, 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF, 0x1231, 0x0210, 0x3273, 0x2252, 0x52B5, 0x4294,
        0x72F7, 0x62D6, 0x9339, 0x8318, 0xB37B, 0xA35A, 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, 0x2462,
        0x3443, 0x0420, 0x1401, 0x64E6, 0x74C7, 0x44A4, 0x5485, 0xA56A, 0xB54B, 0x8528, 0x9509,
        0xE5EE, 0xF5CF, 0xC5AC, 0xD58D, 0x3653, 0x2672, 0x1611, 0x0630, 0x76D7, 0x66F6, 0x5695,
        0x46B4, 0xB75B, 0xA77A, 0x9719, 0x8738, 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, 0x48C4, 0x58E5,
        0x6886, 0x78A7, 0x0840, 0x1861, 0x2802, 0x3823, 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, 0x8948,
        0x9969, 0xA90A, 0xB92B, 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, 0x1A71, 0x0A50, 0x3A33, 0x2A12,
        0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, 0x9B79, 0x8B58, 0xBB3B, 0xAB1A, 0x6CA6, 0x7C87, 0x4CE4,
        0x5CC5, 0x2C22, 0x3C03, 0x0C60, 0x1C41, 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, 0xAD2A, 0xBD0B,
        0x8D68, 0x9D49, 0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, 0x3E13, 0x2E32, 0x1E51, 0x0E70, 0xFF9F,
        0xEFBE, 0xDFDD, 0xCFFC, 0xBF1B, 0xAF3A, 0x9F59, 0x8F78, 0x9188, 0x81A9, 0xB1CA, 0xA1EB,
        0xD10C, 0xC12D, 0xF14E, 0xE16F, 0x1080, 0x00A1, 0x30C2, 0x20E3, 0x5004, 0x4025, 0x7046,
        0x6067, 0x83B9, 0x9398, 0xA3FB, 0xB3DA, 0xC33D, 0xD31C, 0xE37F, 0xF35E, 0x02B1, 0x1290,
        0x22F3, 0x32D2, 0x4235, 0x5214, 0x6277, 0x7256, 0xB5EA, 0xA5CB, 0x95A8, 0x8589, 0xF56E,
        0xE54F, 0xD52C, 0xC50D, 0x34E2, 0x24C3, 0x14A0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
        0xA7DB, 0xB7FA, 0x8799, 0x97B8, 0xE75F, 0xF77E, 0xC71D, 0xD73C, 0x26D3, 0x36F2, 0x0691,
        0x16B0, 0x6657, 0x7676, 0x4615, 0x5634, 0xD94C, 0xC96D, 0xF90E, 0xE92F, 0x99C8, 0x89E9,
        0xB98A, 0xA9AB, 0x5844, 0x4865, 0x7806, 0x6827, 0x18C0, 0x08E1, 0x3882, 0x28A3, 0xCB7D,
        0xDB5C, 0xEB3F, 0xFB1E, 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, 0x4A75, 0x5A54, 0x6A37, 0x7A16,
        0x0AF1, 0x1AD0, 0x2AB3, 0x3A92, 0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, 0xBDAA, 0xAD8B, 0x9DE8,
        0x8DC9, 0x7C26, 0x6C07, 0x5C64, 0x4C45, 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1, 0xEF1F, 0xFF3E,
        0xCF5D, 0xDF7C, 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8, 0x6E17, 0x7E36, 0x4E55, 0x5E74, 0x2E93,
        0x3EB2, 0x0ED1, 0x1EF0
    ]

    crc = 0x0000
    for byte in data:
        crc = (crc << 8) ^ table[(crc >> 8) ^ byte]
        crc &= 0xFFFF   # important, crc must stay 16bits all the way through
    return crc


def is_valid_bin(_bin):
    # Check if firmware binary has a valid xmodem CRC at the last two bytes
    if (crc16_xmodem(_bin) != 0):
        logging.error("Invalid firmware CRC! Maybe you picked a wrong file?")
        return False

    return True


def fw_flash(_bin, i2cBus):
    logging.info("Start flashing ...")

    bus = SMBus(i2cBus)

    for idx, data in enumerate(_bin):
        wr = i2c_msg.write(BL_I2C_DEV, [data])
        rd = i2c_msg.read(BL_I2C_DEV, 1)

        try:
            bus.i2c_rdwr(wr, rd)    # Combined write-read op
        except Exception as err:
            # Reset device
            log_flash_error(f"No i2c device found at {i2cBus}-{BL_I2C_DEV:04x}. Bootloader or Reset missing", idx, len(_bin))    # noqa: E501
            system(f'i2cdetect -a -y {i2cBus}')
            raise err

        if list(rd)[0] != data:
            log_flash_error(f"Expected {hex(data)} but received {hex(list(rd)[0])}", idx, len(_bin))
            return FLASH_FAIL

    logging.info("Firmware flashing completed")
    return FLASH_OK


def main():
    logging.basicConfig(level=logging.INFO, format='[%(levelname)s] %(message)s')

    parser = ArgumentParser(
            prog='i2c_flash.py',
            description='Script to flash mule-ATtiny firmware through I2C-Flashloader',
            epilog='')

    parser.add_argument('-f', '--firmware', required=True, help="Path to the firmware binary file")
    parser.add_argument('-c', '--boot_req_gpiochip', type=int, required=True, help="Boot request gpiochip index number (e.g. 0 if /dev/gpiochip0)/")   # noqa: E501
    parser.add_argument('-g', '--boot_req_gpio', type=int, required=True, help="Boot request GPIO index in the gpiochip (e.g. if GPIO3_A5, boot_req_gpiochip is 3 and boot_req_gpio is 5")  # noqa: E501
    parser.add_argument('-b', '--i2c_bus', type=int, required=True, help="I2C bus index (e.g. 2 if /dev/i2c-2) connected to ATtiny")    # noqa: E501
    parser.add_argument('-rc', '--updi_reset_gpiochip', type=int, required=True, help="Updi_reset gpiochip index number (e.g. 0 if /dev/gpiochip0)/")   # noqa: E501
    parser.add_argument('-rg', '--updi_reset_gpio', type=int, required=True, help="Updi_reset GPIO index in the gpiochip (e.g. if GPIO3_A4, boot_req_gpiochip is 3 and boot_req_gpio is 4")  # noqa: E501

    # Get binarry path from cli
    args = parser.parse_args()

    # Open binary file and copy data to "_bin" bytearray
    _bin = load_bin(args.firmware)

    if not is_valid_bin(_bin):
        sys.exit(INVALID_BIN)

    # Make sure kernel doesn't access the isl1208 address while flashing
    if pathlib.Path(f"/sys/bus/i2c/drivers/rtc-isl1208/{args.i2c_bus}-{BL_I2C_DEV:04x}").exists():
        system(f"echo {args.i2c_bus}-{BL_I2C_DEV:04x} > /sys/bus/i2c/drivers/rtc-isl1208/unbind")

    bootCtrl = BootModeController(
        args.boot_req_gpiochip, args.boot_req_gpio, args.updi_reset_gpiochip, args.updi_reset_gpio
    )

    try:
        # Request flashloader through bootloader (Set to read-only-mode by default)
        bootCtrl.enterFlashloader()

        bus = SMBus(args.i2c_bus)
        bl_raw_data = []
        while True:
            try:
                # Read bl_data byte per byte
                bl_raw_data.append(bus.read_byte(BL_I2C_DEV))
            except IOError:
                # Bootloader finished sending bl_data
                break

        bl_data = BLParser.parse(bl_raw_data)

        for d in bl_data:
            logging.info(d)

        bootCtrl.exitRoMode()
        ret = fw_flash(_bin, args.i2c_bus)

    except Exception as err:
        raise err

    finally:
        bootCtrl.exitFlashloader()

    sys.exit(ret)


if __name__ == "__main__":
    main()