mirror of
https://github.com/google/pebble.git
synced 2025-05-23 19:54:53 +00:00
Import of the watch repository from Pebble
This commit is contained in:
commit
3b92768480
10334 changed files with 2564465 additions and 0 deletions
15
python_libs/pblprog/pebble/__init__.py
Normal file
15
python_libs/pblprog/pebble/__init__.py
Normal file
|
@ -0,0 +1,15 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
__import__('pkg_resources').declare_namespace(__name__)
|
44
python_libs/pblprog/pebble/programmer/__init__.py
Normal file
44
python_libs/pblprog/pebble/programmer/__init__.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from targets import STM32F4FlashProgrammer, STM32F7FlashProgrammer
|
||||
from swd_port import SerialWireDebugPort
|
||||
from ftdi_swd import FTDISerialWireDebug
|
||||
|
||||
def get_device(board, reset=True, frequency=None):
|
||||
boards = {
|
||||
'silk_bb': (0x7893, 10E6, STM32F4FlashProgrammer),
|
||||
'robert_bb2': (0x7894, 3E6, STM32F7FlashProgrammer)
|
||||
}
|
||||
|
||||
if board not in boards:
|
||||
raise Exception('Invalid board: {}'.format(board))
|
||||
|
||||
usb_pid, default_frequency, board_ctor = boards[board]
|
||||
if not frequency:
|
||||
frequency = default_frequency
|
||||
|
||||
ftdi = FTDISerialWireDebug(vid=0x0403, pid=usb_pid, interface=0, direction=0x1b,
|
||||
output_mask=0x02, reset_mask=0x40, frequency=frequency)
|
||||
swd_port = SerialWireDebugPort(ftdi, reset)
|
||||
return board_ctor(swd_port)
|
||||
|
||||
def flash(board, hex_files):
|
||||
with get_device(board) as programmer:
|
||||
programmer.execute_loader()
|
||||
for hex_file in hex_files:
|
||||
programmer.load_hex(hex_file)
|
||||
|
||||
programmer.reset_core()
|
||||
|
38
python_libs/pblprog/pebble/programmer/__main__.py
Normal file
38
python_libs/pblprog/pebble/programmer/__main__.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
|
||||
from . import flash
|
||||
|
||||
def main(args=None):
|
||||
if args is None:
|
||||
parser = argparse.ArgumentParser(description='A tool for flashing a bigboard via FTDI+SWD')
|
||||
parser.add_argument('hex_files', type=str, nargs='+',
|
||||
help='Path to one or more hex files to flash')
|
||||
parser.add_argument('--board', action='store', choices=['robert_bb2', 'silk_bb'], required=True,
|
||||
help='Which board is being programmed')
|
||||
parser.add_argument('--verbose', action='store_true',
|
||||
help='Output lots of debugging info to the console.')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
logging.basicConfig(level=(logging.DEBUG if args.verbose else logging.INFO))
|
||||
|
||||
flash(args.board, args.hex_files)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
137
python_libs/pblprog/pebble/programmer/ftdi_swd.py
Normal file
137
python_libs/pblprog/pebble/programmer/ftdi_swd.py
Normal file
|
@ -0,0 +1,137 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from array import array
|
||||
|
||||
from pyftdi.pyftdi.ftdi import Ftdi
|
||||
import usb.util
|
||||
|
||||
class FTDISerialWireDebug(object):
|
||||
def __init__(self, vid, pid, interface, direction, output_mask, reset_mask, frequency):
|
||||
self._direction = direction
|
||||
self._output_mask = output_mask
|
||||
self._reset_mask = reset_mask
|
||||
self._ftdi = Ftdi()
|
||||
try:
|
||||
self._ftdi.open_mpsse(vid, pid, interface, direction=direction, frequency=frequency,
|
||||
latency=1)
|
||||
except:
|
||||
self._ftdi = None
|
||||
raise
|
||||
|
||||
# get the FTDI FIFO size and increase the chuncksize to match
|
||||
self._ftdi_fifo_size = min(self._ftdi.fifo_sizes)
|
||||
self._ftdi.write_data_set_chunksize(self._ftdi_fifo_size)
|
||||
|
||||
self._cmd_buffer = array('B')
|
||||
self._output_enabled = False
|
||||
self._pending_acks = 0
|
||||
self._sequence_cmd_buffer = None
|
||||
|
||||
def close(self):
|
||||
if not self._ftdi:
|
||||
return
|
||||
self.send_cmds()
|
||||
self._ftdi.close()
|
||||
# PyFTDI doesn't do a good job of cleaning up - make sure we release the usb device
|
||||
usb.util.dispose_resources(self._ftdi.usb_dev)
|
||||
self._ftdi = None
|
||||
|
||||
def _fatal(self, message):
|
||||
raise Exception('FATAL ERROR: {}'.format(message))
|
||||
|
||||
def _queue_cmd(self, write_data):
|
||||
if len(write_data) > self._ftdi_fifo_size:
|
||||
raise Exception('Data too big!')
|
||||
if self._sequence_cmd_buffer is not None:
|
||||
self._sequence_cmd_buffer.extend(write_data)
|
||||
else:
|
||||
if len(self._cmd_buffer) + len(write_data) > self._ftdi_fifo_size:
|
||||
self.send_cmds()
|
||||
self._cmd_buffer.extend(write_data)
|
||||
|
||||
def _set_output_enabled(self, enabled):
|
||||
if enabled == self._output_enabled:
|
||||
return
|
||||
self._output_enabled = enabled
|
||||
direction = self._direction & ~(0x00 if enabled else self._output_mask)
|
||||
self._queue_cmd([Ftdi.SET_BITS_LOW, 0, direction])
|
||||
|
||||
def reset(self):
|
||||
# toggle the reset line
|
||||
self.reset_lo()
|
||||
self.reset_hi()
|
||||
|
||||
def reset_lo(self):
|
||||
direction = self._direction & ~(0x00 if self._output_enabled else self._output_mask)
|
||||
self._queue_cmd([Ftdi.SET_BITS_LOW, 0, direction | self._reset_mask])
|
||||
self.send_cmds()
|
||||
|
||||
def reset_hi(self):
|
||||
direction = self._direction & ~(0x00 if self._output_enabled else self._output_mask)
|
||||
self._queue_cmd([Ftdi.SET_BITS_LOW, 0, direction & ~self._reset_mask])
|
||||
self.send_cmds()
|
||||
|
||||
def send_cmds(self):
|
||||
if self._sequence_cmd_buffer is not None:
|
||||
self._ftdi.write_data(self._sequence_cmd_buffer)
|
||||
elif len(self._cmd_buffer) > 0:
|
||||
self._ftdi.write_data(self._cmd_buffer)
|
||||
self._cmd_buffer = array('B')
|
||||
|
||||
def write_bits_cmd(self, data, num_bits):
|
||||
if num_bits < 0 or num_bits > 8:
|
||||
self._fatal('Invalid num_bits')
|
||||
elif (data & ((1 << num_bits) - 1)) != data:
|
||||
self._fatal('Invalid data!')
|
||||
self._set_output_enabled(True)
|
||||
self._queue_cmd([Ftdi.WRITE_BITS_NVE_LSB, num_bits - 1, data])
|
||||
|
||||
def write_bytes_cmd(self, data):
|
||||
length = len(data) - 1
|
||||
if length < 0 or length > 0xffff:
|
||||
self._fatal('Invalid length')
|
||||
self._set_output_enabled(True)
|
||||
self._queue_cmd([Ftdi.WRITE_BYTES_NVE_LSB, length & 0xff, length >> 8] + data)
|
||||
|
||||
def read_bits_cmd(self, num_bits):
|
||||
if num_bits < 0 or num_bits > 8:
|
||||
self._fatal('Invalid num_bits')
|
||||
self._set_output_enabled(False)
|
||||
self._queue_cmd([Ftdi.READ_BITS_PVE_LSB, num_bits - 1])
|
||||
|
||||
def read_bytes_cmd(self, length):
|
||||
length -= 1
|
||||
if length < 0 or length > 0xffff:
|
||||
self._fatal('Invalid length')
|
||||
self._set_output_enabled(False)
|
||||
self._queue_cmd([Ftdi.READ_BYTES_PVE_LSB, length & 0xff, length >> 8])
|
||||
|
||||
def get_read_bytes(self, length):
|
||||
return self._ftdi.read_data_bytes(length)
|
||||
|
||||
def get_read_fifo_size(self):
|
||||
return self._ftdi_fifo_size
|
||||
|
||||
def start_sequence(self):
|
||||
if self._sequence_cmd_buffer is not None:
|
||||
self._fatal('Attempted to start a sequence while one is in progress')
|
||||
self.send_cmds()
|
||||
self._sequence_cmd_buffer = array('B')
|
||||
|
||||
def end_sequence(self):
|
||||
if self._sequence_cmd_buffer is None:
|
||||
self._fatal('No sequence started')
|
||||
self._sequence_cmd_buffer = None
|
||||
|
287
python_libs/pblprog/pebble/programmer/swd_port.py
Normal file
287
python_libs/pblprog/pebble/programmer/swd_port.py
Normal file
|
@ -0,0 +1,287 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import struct
|
||||
import time
|
||||
|
||||
class SerialWireDebugPort(object):
|
||||
# debug port registers
|
||||
DP_IDCODE_ADDR = 0x00
|
||||
DP_ABORT_ADDR = 0x00
|
||||
DP_CTRLSTAT_ADDR = 0x04
|
||||
DP_SELECT_ADDR = 0x08
|
||||
DP_RDBUFF_ADDR = 0x0c
|
||||
|
||||
# MEM-AP register
|
||||
MEM_AP_CSW_ADDR = 0x0
|
||||
MEM_AP_CSW_MASTER_DEBUG = (1 << 29)
|
||||
MEM_AP_CSW_PRIVILEGED_MODE = (1 << 25)
|
||||
MEM_AP_CSW_ADDRINCWORD = (1 << 4)
|
||||
MEM_AP_CSW_SIZE8BITS = (0 << 1)
|
||||
MEM_AP_CSW_SIZE32BITS = (1 << 1)
|
||||
|
||||
MEM_AP_TAR_ADDR = 0x4
|
||||
MEM_AP_DRW_ADDR = 0xc
|
||||
MEM_AP_IDR_VALUES = [0x24770011, 0x74770001]
|
||||
|
||||
def __init__(self, driver, reset=True):
|
||||
self._driver = driver
|
||||
self._swd_connected = False
|
||||
self._reset = reset
|
||||
self._pending_acks = 0
|
||||
|
||||
def close(self):
|
||||
if self._swd_connected:
|
||||
# power down the system and debug domains
|
||||
self._write(self.DP_CTRLSTAT_ADDR, 0x00000000, is_access_port=False)
|
||||
# send 1 byte worth of trailing bits since we're done communicating
|
||||
self._driver.write_bits_cmd(0x00, 8)
|
||||
# this makes the Saleae's SWD analyzer happy, and it's otherwise harmless
|
||||
self._driver.write_bits_cmd(0x3, 2)
|
||||
self._swd_connected = False
|
||||
self._driver.close()
|
||||
|
||||
@staticmethod
|
||||
def _fatal(message):
|
||||
raise Exception('FATAL ERROR: {}'.format(message))
|
||||
|
||||
def _get_request_header(self, addr, is_read, is_access_port):
|
||||
# the header consists of the following fields
|
||||
# bit 0: start (always 1)
|
||||
# bit 1: DebugPort (0) or AccessPort (1)
|
||||
# bit 2: write (0) or read (1)
|
||||
# bits 3-4: bits 2 and 3 of the address
|
||||
# bit 5: parity bit such that bits 1-5 contain an even number of 1's
|
||||
# bit 6: stop (always 0)
|
||||
# bit 7: park (always 1)
|
||||
header = 0x1
|
||||
header |= (1 << 1) if is_access_port else 0
|
||||
header |= (1 << 2) if is_read else 0
|
||||
header |= ((addr & 0xf) >> 2) << 3
|
||||
parity = 0
|
||||
for i in range(1, 5):
|
||||
parity += (header >> i) & 0x1
|
||||
header |= (parity & 0x1) << 5
|
||||
header |= 1 << 7
|
||||
return header
|
||||
|
||||
def _send_request_header(self, addr, is_read, is_access_port):
|
||||
self._driver.write_bytes_cmd([self._get_request_header(addr, is_read, is_access_port)])
|
||||
|
||||
def _check_write_acks(self):
|
||||
if not self._pending_acks:
|
||||
return
|
||||
self._driver.send_cmds()
|
||||
# the ACK is in the top 3 bits that we get from the FTDI read, so shift right by 5
|
||||
for ack in [x >> 5 for x in self._driver.get_read_bytes(self._pending_acks)]:
|
||||
if ack != 0x1:
|
||||
self._fatal('ACK=0x{:02x}'.format(ack))
|
||||
self._pending_acks = 0
|
||||
|
||||
def _read(self, addr, is_access_port):
|
||||
# check any pending write ACKs before doing a read
|
||||
self._check_write_acks()
|
||||
|
||||
# send the read request
|
||||
self._send_request_header(addr, is_read=True, is_access_port=is_access_port)
|
||||
|
||||
# do all the reads at the same time as an optimization (and hope we get an ACK)
|
||||
self._driver.read_bits_cmd(4) # 4 bits for ACK + turnaround
|
||||
self._driver.read_bytes_cmd(4) # 4 data bytes
|
||||
self._driver.read_bits_cmd(2) # 2 bits for parity + turnaround
|
||||
self._driver.send_cmds()
|
||||
result = self._driver.get_read_bytes(6)
|
||||
|
||||
# check the ACK
|
||||
ack = result[0] >> 5
|
||||
if ack != 0x1:
|
||||
self._fatal('ACK=0x{:02x}'.format(ack))
|
||||
|
||||
# grab the response
|
||||
response = struct.unpack('<I', result[1:5])[0]
|
||||
|
||||
# read two more bits: the parity and another for some reason I don't understand
|
||||
# check that the parity is correct
|
||||
parity = (result[5] >> 6) & 0x1
|
||||
if parity != sum((response >> i) & 0x1 for i in range(32)) & 0x1:
|
||||
self._fatal('Bad parity')
|
||||
|
||||
return response
|
||||
|
||||
def _write(self, addr, data, is_access_port, no_ack=False):
|
||||
if data > 0xffffffff:
|
||||
self._fatal('Bad data')
|
||||
|
||||
# send the write request
|
||||
self._send_request_header(addr, is_read=False, is_access_port=is_access_port)
|
||||
|
||||
# OPTIMIZATION: queue the ACK read now and keep going (hope we get an ACK)
|
||||
self._driver.read_bits_cmd(4)
|
||||
|
||||
# calculate the parity and send the data
|
||||
parity = sum((data >> i) & 0x1 for i in range(32)) & 0x1
|
||||
# OPTIMIZATION: We need to send 1 turnaround bit, 4 data bytes, and 1 parity bit.
|
||||
# We can combine this into a single FTDI write by sending it as 5 bytes, so
|
||||
# let's shift everything such that the extra 6 bits are at the end where they
|
||||
# will be properly ignored as trailing bits.
|
||||
temp = ((data << 1) & 0xfffffffe)
|
||||
data_bytes = [(temp >> (i * 8)) & 0xff for i in range(4)]
|
||||
data_bytes += [(data >> 31) | (parity << 1)]
|
||||
self._driver.write_bytes_cmd(data_bytes)
|
||||
|
||||
# check the ACK(s) if necessary
|
||||
self._pending_acks += 1
|
||||
if not no_ack or self._pending_acks >= self._driver.get_read_fifo_size():
|
||||
self._check_write_acks()
|
||||
|
||||
def connect(self):
|
||||
if self._reset:
|
||||
# reset the target
|
||||
self._driver.reset_lo()
|
||||
|
||||
# switch from JTAG to SWD mode (based on what openocd does)
|
||||
# - line reset
|
||||
# - magic number of 0xE79E
|
||||
# - line reset
|
||||
# - 2 low bits for unknown reasons (maybe padding to nibbles?)
|
||||
def line_reset():
|
||||
# a line reset is 50 high bits (6 bytes + 2 bits)
|
||||
self._driver.write_bytes_cmd([0xff] * 6)
|
||||
self._driver.write_bits_cmd(0x3, 2)
|
||||
line_reset()
|
||||
self._driver.write_bytes_cmd([0x9e, 0xe7])
|
||||
line_reset()
|
||||
self._driver.write_bits_cmd(0x0, 2)
|
||||
|
||||
idcode = self._read(self.DP_IDCODE_ADDR, is_access_port=False)
|
||||
|
||||
# clear the error flags
|
||||
self._write(self.DP_ABORT_ADDR, 0x0000001E, is_access_port=False)
|
||||
|
||||
# power up the system and debug domains
|
||||
self._write(self.DP_CTRLSTAT_ADDR, 0xF0000001, is_access_port=False)
|
||||
|
||||
# check the MEM-AP IDR
|
||||
# the IDR register is has the same address as the DRW register but on the 0xf bank
|
||||
self._write(self.DP_SELECT_ADDR, 0xf0, is_access_port=False) # select the 0xf bank
|
||||
self._read(self.MEM_AP_DRW_ADDR, is_access_port=True) # read the value register (twice)
|
||||
if self._read(self.DP_RDBUFF_ADDR, is_access_port=False) not in self.MEM_AP_IDR_VALUES:
|
||||
self._fatal('Invalid MEM-AP IDR')
|
||||
self._write(self.DP_SELECT_ADDR, 0x0, is_access_port=False) # return to the 0x0 bank
|
||||
|
||||
# enable privileged access to the MEM-AP with 32 bit data accesses and auto-incrementing
|
||||
csw_value = self.MEM_AP_CSW_PRIVILEGED_MODE
|
||||
csw_value |= self.MEM_AP_CSW_MASTER_DEBUG
|
||||
csw_value |= self.MEM_AP_CSW_ADDRINCWORD
|
||||
csw_value |= self.MEM_AP_CSW_SIZE32BITS
|
||||
self._write(self.MEM_AP_CSW_ADDR, csw_value, is_access_port=True)
|
||||
|
||||
self._swd_connected = True
|
||||
if self._reset:
|
||||
self._driver.reset_hi()
|
||||
return idcode
|
||||
|
||||
def read_memory_address(self, addr):
|
||||
self._write(self.MEM_AP_TAR_ADDR, addr, is_access_port=True)
|
||||
self._read(self.MEM_AP_DRW_ADDR, is_access_port=True)
|
||||
return self._read(self.DP_RDBUFF_ADDR, is_access_port=False)
|
||||
|
||||
def write_memory_address(self, addr, value):
|
||||
self._write(self.MEM_AP_TAR_ADDR, addr, is_access_port=True)
|
||||
self._write(self.MEM_AP_DRW_ADDR, value, is_access_port=True)
|
||||
|
||||
def write_memory_bulk(self, base_addr, data):
|
||||
# TAR is configured as auto-incrementing, but it wraps every 4096 bytes, so that's how much
|
||||
# we can write before we need to explicitly set it again.
|
||||
WORD_SIZE = 4
|
||||
BURST_LENGTH = 4096 / WORD_SIZE
|
||||
assert(base_addr % BURST_LENGTH == 0 and len(data) % WORD_SIZE == 0)
|
||||
for i in range(0, len(data), WORD_SIZE):
|
||||
if i % BURST_LENGTH == 0:
|
||||
# set the target address
|
||||
self._write(self.MEM_AP_TAR_ADDR, base_addr + i, is_access_port=True, no_ack=True)
|
||||
# write the word
|
||||
word = sum(data[i+j] << (j * 8) for j in range(WORD_SIZE))
|
||||
self._write(self.MEM_AP_DRW_ADDR, word, is_access_port=True, no_ack=True)
|
||||
|
||||
def continuous_read(self, addr, duration):
|
||||
# This is a highly-optimized function which is samples the specified memory address for the
|
||||
# specified duration. This is generally used for profiling by reading the PC sampling
|
||||
# register.
|
||||
|
||||
NUM_READS = 510 # a magic number which gives us the best sample rate on Silk/Robert
|
||||
|
||||
# don't auto-increment the address
|
||||
csw_value = self.MEM_AP_CSW_PRIVILEGED_MODE
|
||||
csw_value |= self.MEM_AP_CSW_SIZE32BITS
|
||||
self._write(self.MEM_AP_CSW_ADDR, csw_value, is_access_port=True)
|
||||
|
||||
# set the address
|
||||
self._write(self.MEM_AP_TAR_ADDR, addr, is_access_port=True)
|
||||
|
||||
# discard the previous value
|
||||
self._read(self.MEM_AP_DRW_ADDR, is_access_port=True)
|
||||
|
||||
# flush everything
|
||||
self._check_write_acks()
|
||||
|
||||
header = self._get_request_header(self.MEM_AP_DRW_ADDR, is_read=True, is_access_port=True)
|
||||
self._driver.start_sequence()
|
||||
for i in range(NUM_READS):
|
||||
self._driver.write_bits_cmd(header, 8)
|
||||
self._driver.read_bits_cmd(6)
|
||||
self._driver.read_bytes_cmd(4)
|
||||
|
||||
raw_data = []
|
||||
end_time = time.time() + duration
|
||||
while time.time() < end_time:
|
||||
# send the read requests
|
||||
self._driver.send_cmds()
|
||||
# do all the reads at the same time as an optimization (and hope we get an ACK)
|
||||
raw_data.extend(self._driver.get_read_bytes(5 * NUM_READS))
|
||||
self._driver.end_sequence()
|
||||
|
||||
def get_value_from_bits(b):
|
||||
result = 0
|
||||
for o in range(len(b)):
|
||||
result |= b[o] << o
|
||||
return result
|
||||
values = []
|
||||
for raw_result in [raw_data[i:i+5] for i in range(0, len(raw_data), 5)]:
|
||||
result = raw_result
|
||||
# The result is read as 5 bytes, with the first one containing 6 bits (shifted in from
|
||||
# the left as they are read). Let's convert this into an array of bits, and then
|
||||
# reconstruct the values we care about.
|
||||
bits = []
|
||||
bits.extend((result[0] >> (2 + j)) & 1 for j in range(6))
|
||||
for i in range(4):
|
||||
bits.extend((result[i + 1] >> j) & 1 for j in range(8))
|
||||
ack = get_value_from_bits(bits[1:4])
|
||||
response = get_value_from_bits(bits[4:36])
|
||||
parity = bits[36]
|
||||
|
||||
# check the ACK
|
||||
if ack != 0x1:
|
||||
self._fatal('ACK=0x{:02x}'.format(ack))
|
||||
|
||||
# read two more bits: the parity and another for some reason I don't understand
|
||||
# check that the parity is correct
|
||||
if parity != sum((response >> i) & 0x1 for i in range(32)) & 0x1:
|
||||
self._fatal('Bad parity')
|
||||
|
||||
# store the response
|
||||
values.append(response)
|
||||
|
||||
return values
|
||||
|
16
python_libs/pblprog/pebble/programmer/targets/__init__.py
Normal file
16
python_libs/pblprog/pebble/programmer/targets/__init__.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .stm32f4 import STM32F4FlashProgrammer
|
||||
from .stm32f7 import STM32F7FlashProgrammer
|
352
python_libs/pblprog/pebble/programmer/targets/stm32.py
Normal file
352
python_libs/pblprog/pebble/programmer/targets/stm32.py
Normal file
|
@ -0,0 +1,352 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import array
|
||||
import logging
|
||||
import os
|
||||
import struct
|
||||
import time
|
||||
from intelhex import IntelHex
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
class STM32FlashProgrammer(object):
|
||||
# CPUID register
|
||||
CPUID_ADDR = 0xE000ED00
|
||||
|
||||
# Flash constants
|
||||
FLASH_BASE_ADDR = 0x08000000
|
||||
|
||||
# Flash key register (FLASH_KEYR)
|
||||
FLASH_KEYR_ADDR = 0x40023C04
|
||||
FLASH_KEYR_VAL1 = 0x45670123
|
||||
FLASH_KEYR_VAL2 = 0xCDEF89AB
|
||||
|
||||
# Flash status register (FLASH_SR)
|
||||
FLASH_SR_ADDR = 0x40023C0C
|
||||
FLASH_SR_BSY = (1 << 16)
|
||||
|
||||
# Flash control register (FLASH_CR)
|
||||
FLASH_CR_ADDR = 0x40023C10
|
||||
FLASH_CR_PG = (1 << 0)
|
||||
FLASH_CR_SER = (1 << 1)
|
||||
FLASH_CR_SNB_OFFSET = 3
|
||||
FLASH_CR_PSIZE_8BIT = (0x0 << 8)
|
||||
FLASH_CR_PSIZE_16BIT = (0x1 << 8)
|
||||
FLASH_CR_PSIZE_32BIT = (0x2 << 8)
|
||||
FLASH_CR_STRT = (1 << 16)
|
||||
|
||||
# Debug halting control and status register (DHCSR)
|
||||
DHCSR_ADDR = 0xE000EDF0
|
||||
DHCSR_DBGKEY_VALUE = 0xA05F0000
|
||||
DHCSR_HALT = (1 << 0)
|
||||
DHCSR_DEBUGEN = (1 << 1)
|
||||
DHCSR_S_REGRDY = (1 << 16)
|
||||
DHCSR_S_LOCKUP = (1 << 19)
|
||||
|
||||
# Application interrupt and reset control register (AIRCR)
|
||||
AIRCR_ADDR = 0xE000ED0C
|
||||
AIRCR_VECTKEY_VALUE = 0x05FA0000
|
||||
AIRCR_SYSRESETREQ = (1 << 2)
|
||||
|
||||
# Debug Core Register Selector Register (DCRSR)
|
||||
DCRSR_ADDR = 0xE000EDF4
|
||||
DCRSR_WRITE = (1 << 16)
|
||||
|
||||
# Debug Core Register Data register (DCRDR)
|
||||
DCRDR_ADDR = 0xE000EDF8
|
||||
|
||||
# Debug Exception and Monitor Control register (DEMCR)
|
||||
DEMCR_ADDR = 0xE000EDFC
|
||||
DEMCR_RESET_CATCH = (1 << 0)
|
||||
DEMCR_TRCENA = (1 << 24)
|
||||
|
||||
# Program Counter Sample Register (PCSR)
|
||||
PCSR_ADDR = 0xE000101C
|
||||
|
||||
# Loader addresses
|
||||
PBLLDR_HEADER_ADDR = 0x20000400
|
||||
PBLLDR_HEADER_OFFSET = PBLLDR_HEADER_ADDR + 0x4
|
||||
PBLLDR_HEADER_LENGTH = PBLLDR_HEADER_ADDR + 0x8
|
||||
PBLLDR_DATA_ADDR = 0x20000800
|
||||
PBLLDR_DATA_MAX_LENGTH = 0x20000
|
||||
PBLLDR_STATE_WAIT = 0
|
||||
PBLLDR_STATE_WRITE = 1
|
||||
PBLLDR_STATE_CRC = 2
|
||||
|
||||
# SRAM base addr
|
||||
SRAM_BASE_ADDR = 0x20000000
|
||||
|
||||
def __init__(self, driver):
|
||||
self._driver = driver
|
||||
self._step_start_time = 0
|
||||
self.FLASH_SECTOR_SIZES = [x*1024 for x in self.FLASH_SECTOR_SIZES]
|
||||
|
||||
def __enter__(self):
|
||||
try:
|
||||
self.connect()
|
||||
return self
|
||||
except:
|
||||
self.close()
|
||||
raise
|
||||
|
||||
def __exit__(self, exc, value, trace):
|
||||
self.close()
|
||||
|
||||
def _fatal(self, message):
|
||||
raise Exception('FATAL ERROR: {}'.format(message))
|
||||
|
||||
def _start_step(self, msg):
|
||||
LOG.info(msg)
|
||||
self._step_start_time = time.time()
|
||||
|
||||
def _end_step(self, msg, no_time=False, num_bytes=None):
|
||||
total_time = round(time.time() - self._step_start_time, 2)
|
||||
if not no_time:
|
||||
msg += ' in {}s'.format(total_time)
|
||||
if num_bytes:
|
||||
kibps = round(num_bytes / 1024.0 / total_time, 2)
|
||||
msg += ' ({} KiB/s)'.format(kibps)
|
||||
LOG.info(msg)
|
||||
|
||||
def connect(self):
|
||||
self._start_step('Connecting...')
|
||||
|
||||
# connect and check the IDCODE
|
||||
if self._driver.connect() != self.IDCODE:
|
||||
self._fatal('Invalid IDCODE')
|
||||
|
||||
# check the CPUID register
|
||||
if self._driver.read_memory_address(self.CPUID_ADDR) != self.CPUID_VALUE:
|
||||
self._fatal('Invalid CPU ID')
|
||||
|
||||
self._end_step('Connected', no_time=True)
|
||||
|
||||
def halt_core(self):
|
||||
# halt the core immediately
|
||||
dhcsr_value = self.DHCSR_DBGKEY_VALUE | self.DHCSR_DEBUGEN | self.DHCSR_HALT
|
||||
self._driver.write_memory_address(self.DHCSR_ADDR, dhcsr_value)
|
||||
|
||||
def resume_core(self):
|
||||
# resume the core
|
||||
dhcsr_value = self.DHCSR_DBGKEY_VALUE
|
||||
self._driver.write_memory_address(self.DHCSR_ADDR, dhcsr_value)
|
||||
|
||||
def reset_core(self, halt=False):
|
||||
if self._driver.read_memory_address(self.DHCSR_ADDR) & self.DHCSR_S_LOCKUP:
|
||||
# halt the core first to clear the lockup
|
||||
LOG.info('Clearing lockup condition')
|
||||
self.halt_core()
|
||||
|
||||
# enable reset vector catch
|
||||
demcr_value = 0
|
||||
if halt:
|
||||
demcr_value |= self.DEMCR_RESET_CATCH
|
||||
|
||||
self._driver.write_memory_address(self.DEMCR_ADDR, demcr_value)
|
||||
self._driver.read_memory_address(self.DHCSR_ADDR)
|
||||
|
||||
# reset the core
|
||||
aircr_value = self.AIRCR_VECTKEY_VALUE | self.AIRCR_SYSRESETREQ
|
||||
self._driver.write_memory_address(self.AIRCR_ADDR, aircr_value)
|
||||
|
||||
if halt:
|
||||
self.halt_core()
|
||||
|
||||
def unlock_flash(self):
|
||||
# unlock the flash
|
||||
self._driver.write_memory_address(self.FLASH_KEYR_ADDR, self.FLASH_KEYR_VAL1)
|
||||
self._driver.write_memory_address(self.FLASH_KEYR_ADDR, self.FLASH_KEYR_VAL2)
|
||||
|
||||
def _poll_register(self, timeout=0.5):
|
||||
end_time = time.time() + timeout
|
||||
while end_time > time.time():
|
||||
val = self._driver.read_memory_address(self.DHCSR_ADDR)
|
||||
if val & self.DHCSR_S_REGRDY:
|
||||
break
|
||||
else:
|
||||
raise Exception('Register operation was not confirmed')
|
||||
|
||||
def write_register(self, reg, val):
|
||||
self._driver.write_memory_address(self.DCRDR_ADDR, val)
|
||||
reg |= self.DCRSR_WRITE
|
||||
self._driver.write_memory_address(self.DCRSR_ADDR, reg)
|
||||
self._poll_register()
|
||||
|
||||
def read_register(self, reg):
|
||||
self._driver.write_memory_address(self.DCRSR_ADDR, reg)
|
||||
self._poll_register()
|
||||
return self._driver.read_memory_address(self.DCRDR_ADDR)
|
||||
|
||||
def erase_flash(self, flash_offset, length):
|
||||
self._start_step('Erasing...')
|
||||
|
||||
def overlap(a1, a2, b1, b2):
|
||||
return max(a1, b1) < min(a2, b2)
|
||||
|
||||
# find all the sectors which we need to erase
|
||||
erase_sectors = []
|
||||
for i, size in enumerate(self.FLASH_SECTOR_SIZES):
|
||||
addr = self.FLASH_BASE_ADDR + sum(self.FLASH_SECTOR_SIZES[:i])
|
||||
if overlap(flash_offset, flash_offset+length, addr, addr+size):
|
||||
erase_sectors += [i]
|
||||
if not erase_sectors:
|
||||
self._fatal('Could not find sectors to erase!')
|
||||
|
||||
# erase the sectors
|
||||
for sector in erase_sectors:
|
||||
# start the erase
|
||||
reg_value = (sector << self.FLASH_CR_SNB_OFFSET)
|
||||
reg_value |= self.FLASH_CR_PSIZE_8BIT
|
||||
reg_value |= self.FLASH_CR_STRT
|
||||
reg_value |= self.FLASH_CR_SER
|
||||
self._driver.write_memory_address(self.FLASH_CR_ADDR, reg_value)
|
||||
# wait for the erase to finish
|
||||
while self._driver.read_memory_address(self.FLASH_SR_ADDR) & self.FLASH_SR_BSY:
|
||||
time.sleep(0)
|
||||
|
||||
self._end_step('Erased')
|
||||
|
||||
def close(self):
|
||||
self._driver.close()
|
||||
|
||||
def _write_loader_state(self, state):
|
||||
self._driver.write_memory_address(self.PBLLDR_HEADER_ADDR, state)
|
||||
|
||||
def _wait_loader_state(self, wanted_state, timeout=3):
|
||||
end_time = time.time() + timeout
|
||||
state = -1
|
||||
while time.time() < end_time:
|
||||
time.sleep(0)
|
||||
state = self._driver.read_memory_address(self.PBLLDR_HEADER_ADDR)
|
||||
if state == wanted_state:
|
||||
break
|
||||
else:
|
||||
raise Exception("Timed out waiting for loader state %d, got %d" % (wanted_state, state))
|
||||
|
||||
@staticmethod
|
||||
def _chunks(l, n):
|
||||
for i in xrange(0, len(l), n):
|
||||
yield l[i:i+n], len(l[i:i+n]), i
|
||||
|
||||
def execute_loader(self):
|
||||
# reset and halt the core
|
||||
self.reset_core(halt=True)
|
||||
|
||||
with open(os.path.join(os.path.dirname(__file__), "loader.bin")) as f:
|
||||
loader_bin = f.read()
|
||||
|
||||
# load loader binary into SRAM
|
||||
self._driver.write_memory_bulk(self.SRAM_BASE_ADDR, array.array('B', loader_bin))
|
||||
|
||||
# set PC based on value in loader
|
||||
reg_sp, = struct.unpack("<I", loader_bin[:4])
|
||||
self.write_register(13, reg_sp)
|
||||
|
||||
# set PC to new reset handler
|
||||
pc, = struct.unpack('<I', loader_bin[4:8])
|
||||
self.write_register(15, pc)
|
||||
|
||||
# unlock flash
|
||||
self.unlock_flash()
|
||||
|
||||
self.resume_core()
|
||||
|
||||
@staticmethod
|
||||
def generate_crc(data):
|
||||
length = len(data)
|
||||
lookup_table = [0, 47, 94, 113, 188, 147, 226, 205, 87, 120, 9, 38, 235, 196, 181, 154]
|
||||
|
||||
crc = 0
|
||||
for i in xrange(length*2):
|
||||
nibble = data[i / 2]
|
||||
if i % 2 == 0:
|
||||
nibble >>= 4
|
||||
|
||||
index = nibble ^ (crc >> 4)
|
||||
crc = lookup_table[index & 0xf] ^ ((crc << 4) & 0xf0)
|
||||
|
||||
return crc
|
||||
|
||||
def read_crc(self, addr, length):
|
||||
self._driver.write_memory_address(self.PBLLDR_HEADER_OFFSET, addr)
|
||||
self._driver.write_memory_address(self.PBLLDR_HEADER_LENGTH, length)
|
||||
|
||||
self._write_loader_state(self.PBLLDR_STATE_CRC)
|
||||
self._wait_loader_state(self.PBLLDR_STATE_WAIT)
|
||||
return self._driver.read_memory_address(self.PBLLDR_DATA_ADDR) & 0xFF
|
||||
|
||||
def load_hex(self, hex_path):
|
||||
self._start_step("Loading binary: %s" % hex_path)
|
||||
|
||||
ih = IntelHex(hex_path)
|
||||
offset = ih.minaddr()
|
||||
data = ih.tobinarray()
|
||||
|
||||
self.load_bin(offset, data)
|
||||
self._end_step("Loaded binary", num_bytes=len(data))
|
||||
|
||||
def load_bin(self, offset, data):
|
||||
while len(data) % 4 != 0:
|
||||
data.append(0xFF)
|
||||
|
||||
length = len(data)
|
||||
|
||||
# prepare the flash for programming
|
||||
self.erase_flash(offset, length)
|
||||
cr_value = self.FLASH_CR_PSIZE_8BIT | self.FLASH_CR_PG
|
||||
self._driver.write_memory_address(self.FLASH_CR_ADDR, cr_value)
|
||||
|
||||
# set the base address
|
||||
self._wait_loader_state(self.PBLLDR_STATE_WAIT)
|
||||
self._driver.write_memory_address(self.PBLLDR_HEADER_OFFSET, offset)
|
||||
|
||||
for chunk, chunk_length, pos in self._chunks(data, self.PBLLDR_DATA_MAX_LENGTH):
|
||||
LOG.info("Written %d/%d", pos, length)
|
||||
|
||||
self._driver.write_memory_address(self.PBLLDR_HEADER_LENGTH, chunk_length)
|
||||
self._driver.write_memory_bulk(self.PBLLDR_DATA_ADDR, chunk)
|
||||
|
||||
self._write_loader_state(self.PBLLDR_STATE_WRITE)
|
||||
self._wait_loader_state(self.PBLLDR_STATE_WAIT)
|
||||
|
||||
expected_crc = self.generate_crc(data)
|
||||
actual_crc = self.read_crc(offset, length)
|
||||
|
||||
if actual_crc != expected_crc:
|
||||
raise Exception("Bad CRC, expected %d, found %d" % (expected_crc, actual_crc))
|
||||
LOG.info("CRC-8 matched: %d", actual_crc)
|
||||
|
||||
def profile(self, duration):
|
||||
LOG.info('Collecting %f second(s) worth of samples...', duration)
|
||||
|
||||
# ensure DWT is enabled so we can get PC samples from PCSR
|
||||
demcr_value = self._driver.read_memory_address(self.DEMCR_ADDR)
|
||||
self._driver.write_memory_address(self.DEMCR_ADDR, demcr_value | self.DEMCR_TRCENA)
|
||||
|
||||
# take the samples
|
||||
samples = self._driver.continuous_read(self.PCSR_ADDR, duration)
|
||||
|
||||
# restore the original DEMCR value
|
||||
self._driver.write_memory_address(self.DEMCR_ADDR, demcr_value)
|
||||
|
||||
# process the samples
|
||||
pcs = dict()
|
||||
for sample in samples:
|
||||
sample = '0x%08x' % sample
|
||||
pcs[sample] = pcs.get(sample, 0) + 1
|
||||
|
||||
LOG.info('Collected %d samples!', len(samples))
|
||||
return pcs
|
||||
|
21
python_libs/pblprog/pebble/programmer/targets/stm32f4.py
Normal file
21
python_libs/pblprog/pebble/programmer/targets/stm32f4.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from stm32 import STM32FlashProgrammer
|
||||
|
||||
class STM32F4FlashProgrammer(STM32FlashProgrammer):
|
||||
IDCODE = 0x2BA01477
|
||||
CPUID_VALUE = 0x410FC241
|
||||
|
||||
FLASH_SECTOR_SIZES = [16, 16, 16, 16, 64, 128, 128, 128, 128, 128, 128, 128]
|
21
python_libs/pblprog/pebble/programmer/targets/stm32f7.py
Normal file
21
python_libs/pblprog/pebble/programmer/targets/stm32f7.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Copyright 2024 Google LLC
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from stm32 import STM32FlashProgrammer
|
||||
|
||||
class STM32F7FlashProgrammer(STM32FlashProgrammer):
|
||||
IDCODE = 0x5BA02477
|
||||
CPUID_VALUE = 0x411FC270
|
||||
|
||||
FLASH_SECTOR_SIZES = [32, 32, 32, 32, 128, 256, 256, 256, 256, 256, 256, 256]
|
Loading…
Add table
Add a link
Reference in a new issue