From aae870c76749c1a61fde1ae12cf5c3ab8a6bf068 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Sat, 14 Sep 2024 11:29:51 +0200 Subject: [PATCH] Flashloader and UART improvements --- bootloader/src/main.rs | 27 +- flashloader/README.md | 6 + flashloader/image-loader.py | 498 +++++++++++++++++++++--------------- flashloader/src/main.rs | 32 ++- va416xx-hal/CHANGELOG.md | 3 + va416xx-hal/src/uart.rs | 413 +++++++++++++++++++----------- 6 files changed, 608 insertions(+), 371 deletions(-) diff --git a/bootloader/src/main.rs b/bootloader/src/main.rs index 40dbdf5..b010ac9 100644 --- a/bootloader/src/main.rs +++ b/bootloader/src/main.rs @@ -45,17 +45,26 @@ const BOOTLOADER_START_ADDR: u32 = 0x0; const BOOTLOADER_CRC_ADDR: u32 = BOOTLOADER_END_ADDR - 4; const BOOTLOADER_END_ADDR: u32 = 0x4000; -const APP_A_START_ADDR: u32 = BOOTLOADER_END_ADDR; // 0x4000 -// The actual size of the image which is relevant for CRC calculation. -const APP_A_SIZE_ADDR: u32 = APP_B_END_ADDR - 8; // 0x21FF8 -const APP_A_CRC_ADDR: u32 = APP_B_END_ADDR - 4; // 0x21FFC +// 0x4000 +const APP_A_START_ADDR: u32 = BOOTLOADER_END_ADDR; +// The actual size of the image which is relevant for CRC calculation will be store at this +// address. +// 0x21FF8 +const APP_A_SIZE_ADDR: u32 = APP_B_END_ADDR - 8; +// 0x21FFC +const APP_A_CRC_ADDR: u32 = APP_B_END_ADDR - 4; pub const APP_A_END_ADDR: u32 = APP_B_END_ADDR - BOOTLOADER_END_ADDR / 2; -const APP_B_START_ADDR: u32 = APP_A_END_ADDR; // 0x22000 -// The actual size of the image which is relevant for CRC calculation. -const APP_B_SIZE_ADDR: u32 = APP_B_END_ADDR - 8; // 0x3FFF8 -const APP_B_CRC_ADDR: u32 = APP_B_END_ADDR - 4; // 0x3FFFC -pub const APP_B_END_ADDR: u32 = NVM_SIZE; // 0x40000 +// 0x22000 +const APP_B_START_ADDR: u32 = APP_A_END_ADDR; +// The actual size of the image which is relevant for CRC calculation will be stored at this +// address. +// 0x3FFF8 +const APP_B_SIZE_ADDR: u32 = APP_B_END_ADDR - 8; +// 0x3FFFC +const APP_B_CRC_ADDR: u32 = APP_B_END_ADDR - 4; +// 0x40000 +pub const APP_B_END_ADDR: u32 = NVM_SIZE; pub const APP_IMG_SZ: u32 = APP_B_END_ADDR - APP_A_START_ADDR / 2; diff --git a/flashloader/README.md b/flashloader/README.md index f3bb928..885ead1 100644 --- a/flashloader/README.md +++ b/flashloader/README.md @@ -6,12 +6,18 @@ a simple PUS (CCSDS) interface to update the software. It also provides a Python called the `image-loader.py` which can be used to upload compiled images to the flashloader application to write them to the NVM. +Please note that the both the application and the image loader are tailored towards usage +with the [bootloader provided by this repository](https://egit.irs.uni-stuttgart.de/rust/va416xx-rs/src/branch/main/bootloader). + The software can quickly be adapted to interface with a real primary on-board software instead of the Python script provided here to upload images because it uses a low-level CCSDS based packet interface. ## Using the Python image loader +The Python image loader communicates with the Rust flashload application using a dedicated serial +port with a baudrate of 115200. + It is recommended to run the script in a dedicated virtual environment. For example, on UNIX systems you can use `python3 -m venv venv` and then `source venv/bin/activate` to create and activate a virtual environment. diff --git a/flashloader/image-loader.py b/flashloader/image-loader.py index d12b658..802cdec 100755 --- a/flashloader/image-loader.py +++ b/flashloader/image-loader.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 +from typing import List, Tuple from spacepackets.ecss.defs import PusService from spacepackets.ecss.tm import PusTm +from tmtccmd.com import ComInterface import toml import struct import logging @@ -21,20 +23,27 @@ from elftools.elf.elffile import ELFFile BAUD_RATE = 115200 + BOOTLOADER_START_ADDR = 0x0 BOOTLOADER_END_ADDR = 0x4000 -BOOTLOADER_CRC_ADDR = 0x3FFC +BOOTLOADER_CRC_ADDR = BOOTLOADER_END_ADDR - 4 +BOOTLOADER_MAX_SIZE = BOOTLOADER_END_ADDR - BOOTLOADER_START_ADDR - 4 + APP_A_START_ADDR = 0x4000 APP_A_END_ADDR = 0x22000 # The actual size of the image which is relevant for CRC calculation. -APP_A_SIZE_ADDR = 0x21FF8 -APP_A_CRC_ADDR = 0x21FFC +APP_A_SIZE_ADDR = APP_A_END_ADDR - 8 +APP_A_CRC_ADDR = APP_A_END_ADDR - 4 +APP_A_MAX_SIZE = APP_A_END_ADDR - APP_A_START_ADDR - 8 + APP_B_START_ADDR = 0x22000 APP_B_END_ADDR = 0x40000 # The actual size of the image which is relevant for CRC calculation. -APP_B_SIZE_ADDR = 0x3FFF8 -APP_B_CRC_ADDR = 0x3FFFC -APP_IMG_SZ = 0x1E000 +APP_B_SIZE_ADDR = APP_B_END_ADDR - 8 +APP_B_CRC_ADDR = APP_B_END_ADDR - 4 +APP_B_MAX_SIZE = APP_A_END_ADDR - APP_A_START_ADDR - 8 + +APP_IMG_SZ = (APP_B_END_ADDR - APP_A_START_ADDR) // 2 CHUNK_SIZE = 896 @@ -52,6 +61,7 @@ class ActionId(enum.IntEnum): _LOGGER = logging.getLogger(__name__) +SEQ_PROVIDER = SeqCountProvider(bit_width=14) @dataclasses.dataclass @@ -62,7 +72,174 @@ class LoadableSegment: data: bytes -SEQ_PROVIDER = SeqCountProvider(bit_width=14) +class Target(enum.Enum): + BOOTLOADER = 0 + APP_A = 1 + APP_B = 2 + + +class ImageLoader: + def __init__(self, com_if: ComInterface, verificator: PusVerificator) -> None: + self.com_if = com_if + self.verificator = verificator + + def handle_ping_cmd(self): + _LOGGER.info("Sending ping command") + ping_tc = PusTc( + apid=0x00, + service=PusService.S17_TEST, + subservice=1, + seq_count=SEQ_PROVIDER.get_and_increment(), + app_data=bytes(PING_PAYLOAD_SIZE), + ) + self.verificator.add_tc(ping_tc) + self.com_if.send(bytes(ping_tc.pack())) + + data_available = self.com_if.data_available(0.4) + if not data_available: + _LOGGER.warning("no ping reply received") + for reply in self.com_if.receive(): + result = self.verificator.add_tm( + Service1Tm.from_tm(PusTm.unpack(reply, 0), UnpackParams(0)) + ) + if result is not None and result.completed: + _LOGGER.info("received ping completion reply") + + def handle_corruption_cmd(self, target: Target): + + if target == Target.BOOTLOADER: + _LOGGER.error("can not corrupt bootloader") + if target == Target.APP_A: + self.send_tc( + PusTc( + apid=0, + service=ACTION_SERVICE, + subservice=ActionId.CORRUPT_APP_A, + ), + ) + if target == Target.APP_B: + self.send_tc( + PusTc( + apid=0, + service=ACTION_SERVICE, + subservice=ActionId.CORRUPT_APP_B, + ), + ) + + def handle_flash_cmd(self, target: Target, file_path: Path) -> int: + loadable_segments = [] + _LOGGER.info("Parsing ELF file for loadable sections") + total_size = 0 + loadable_segments, total_size = create_loadable_segments(target, file_path) + segments_info_str(target, loadable_segments, total_size, file_path) + result = self._perform_flashing_algorithm(loadable_segments) + if result != 0: + return result + self._crc_and_app_size_postprocessing(target, total_size, loadable_segments) + return 0 + + def _perform_flashing_algorithm( + self, + loadable_segments: List[LoadableSegment], + ) -> int: + # Perform the flashing algorithm. + for segment in loadable_segments: + segment_end = segment.offset + segment.size + current_addr = segment.offset + pos_in_segment = 0 + while pos_in_segment < segment.size: + next_chunk_size = min(segment_end - current_addr, CHUNK_SIZE) + data = segment.data[pos_in_segment : pos_in_segment + next_chunk_size] + next_packet = pack_memory_write_command(current_addr, data) + _LOGGER.info( + f"Sending memory write command for address {current_addr:#08x} and data with " + f"length {len(data)}" + ) + self.verificator.add_tc(next_packet) + self.com_if.send(bytes(next_packet.pack())) + current_addr += next_chunk_size + pos_in_segment += next_chunk_size + start_time = time.time() + while True: + if time.time() - start_time > 1.0: + _LOGGER.error("Timeout while waiting for reply") + return -1 + data_available = self.com_if.data_available(0.1) + done = False + if not data_available: + continue + replies = self.com_if.receive() + for reply in replies: + tm = PusTm.unpack(reply, 0) + if tm.service != 1: + continue + service_1_tm = Service1Tm.from_tm(tm, UnpackParams(0)) + check_result = self.verificator.add_tm(service_1_tm) + # We could send after we have received the step reply, but that can + # somehow lead to overrun errors. I think it's okay to do it like + # this as long as the flash loader only uses polling.. + if ( + check_result is not None + and check_result.status.completed == StatusField.SUCCESS + ): + done = True + + # This is an optimized variant, but I think the small delay is not an issue. + """ + if ( + check_result is not None + and check_result.status.step == StatusField.SUCCESS + and len(check_result.status.step_list) == 1 + ): + done = True + """ + self.verificator.remove_completed_entries() + if done: + break + return 0 + + def _crc_and_app_size_postprocessing( + self, + target: Target, + total_size: int, + loadable_segments: List[LoadableSegment], + ): + if target == Target.BOOTLOADER: + _LOGGER.info("Blanking the bootloader checksum") + # Blank the checksum. For the bootloader, the bootloader will calculate the + # checksum itself on the initial run. + checksum_write_packet = pack_memory_write_command( + BOOTLOADER_CRC_ADDR, bytes([0x00, 0x00, 0x00, 0x00]) + ) + self.send_tc(checksum_write_packet) + else: + crc_addr = None + size_addr = None + if target == Target.APP_A: + crc_addr = APP_A_CRC_ADDR + size_addr = APP_A_SIZE_ADDR + elif target == Target.APP_B: + crc_addr = APP_B_CRC_ADDR + size_addr = APP_B_SIZE_ADDR + assert crc_addr is not None + assert size_addr is not None + _LOGGER.info(f"Writing app size {total_size} at address {size_addr:#08x}") + size_write_packet = pack_memory_write_command( + size_addr, struct.pack("!I", total_size) + ) + self.com_if.send(bytes(size_write_packet.pack())) + time.sleep(0.2) + crc_calc = PredefinedCrc("crc-32") + for segment in loadable_segments: + crc_calc.update(segment.data) + checksum = crc_calc.digest() + _LOGGER.info( + f"Writing checksum 0x[{checksum.hex(sep=',')}] at address {crc_addr:#08x}" + ) + self.send_tc(pack_memory_write_command(crc_addr, checksum)) + + def send_tc(self, tc: PusTc): + self.com_if.send(bytes(tc.pack())) def main() -> int: @@ -102,213 +279,134 @@ def main() -> int: verificator = PusVerificator() com_if = SerialCobsComIF(serial_cfg) com_if.open() + target = None + if args.target == "bl": + target = Target.BOOTLOADER + elif args.target == "a": + target = Target.APP_A + elif args.target == "b": + target = Target.APP_B + image_loader = ImageLoader(com_if, verificator) file_path = None + result = -1 if args.ping: - _LOGGER.info("Sending ping command") - ping_tc = PusTc( - apid=0x00, - service=PusService.S17_TEST, - subservice=1, - seq_count=SEQ_PROVIDER.get_and_increment(), - app_data=bytes(PING_PAYLOAD_SIZE), - ) - verificator.add_tc(ping_tc) - com_if.send(ping_tc.pack()) - - data_available = com_if.data_available(0.4) - if not data_available: - _LOGGER.warning("no ping reply received") - for reply in com_if.receive(): - result = verificator.add_tm( - Service1Tm.from_tm(PusTm.unpack(reply, 0), UnpackParams(0)) - ) - if result is not None and result.completed: - _LOGGER.info("received ping completion reply") - if not args.target: - return 0 - if args.target: + image_loader.handle_ping_cmd() + com_if.close() + return 0 + if target: if not args.corrupt: if not args.path: _LOGGER.error("App Path needs to be specified for the flash process") - return -1 file_path = Path(args.path) if not file_path.exists(): _LOGGER.error("File does not exist") - return -1 if args.corrupt: - if not args.target: + if not target: _LOGGER.error("target for corruption command required") + com_if.close() return -1 - if args.target == "bl": - _LOGGER.error("can not corrupt bootloader") - if args.target == "a": - packet = PusTc( - apid=0, - service=ACTION_SERVICE, - subservice=ActionId.CORRUPT_APP_A, - ) - com_if.send(packet.pack()) - if args.target == "b": - packet = PusTc( - apid=0, - service=ACTION_SERVICE, - subservice=ActionId.CORRUPT_APP_B, - ) - com_if.send(packet.pack()) + image_loader.handle_corruption_cmd(target) else: assert file_path is not None - loadable_segments = [] - _LOGGER.info("Parsing ELF file for loadable sections") - total_size = 0 - with open(file_path, "rb") as app_file: - elf_file = ELFFile(app_file) + assert target is not None + result = image_loader.handle_flash_cmd(target, file_path) - for (idx, segment) in enumerate(elf_file.iter_segments("PT_LOAD")): - if segment.header.p_filesz == 0: - continue - # Basic validity checks of the base addresses. - if idx == 0: - if ( - args.target == "bl" - and segment.header.p_paddr != BOOTLOADER_START_ADDR - ): - raise ValueError( - f"detected possibly invalid start address {segment.header.p_paddr:#08x} for " - f"bootloader, expected {BOOTLOADER_START_ADDR}" - ) - if ( - args.target == "a" - and segment.header.p_paddr != APP_A_START_ADDR - ): - raise ValueError( - f"detected possibly invalid start address {segment.header.p_paddr:#08x} for " - f"App A, expected {APP_A_START_ADDR}" - ) - if ( - args.target == "b" - and segment.header.p_paddr != APP_B_START_ADDR - ): - raise ValueError( - f"detected possibly invalid start address {segment.header.p_paddr:#08x} for " - f"App B, expected {APP_B_START_ADDR}" - ) - name = None - for section in elf_file.iter_sections(): - if ( - section.header.sh_offset == segment.header.p_offset - and section.header.sh_size > 0 - ): - name = section.name - if name is None: - _LOGGER.warning("no fitting section found for segment") - continue - # print(f"Segment Addr: {segment.header.p_paddr}") - # print(f"Segment Offset: {segment.header.p_offset}") - # print(f"Segment Filesize: {segment.header.p_filesz}") - loadable_segments.append( - LoadableSegment( - name=name, - offset=segment.header.p_paddr, - size=segment.header.p_filesz, - data=segment.data(), - ) - ) - total_size += segment.header.p_filesz - context_str = None - if args.target == "bl": - context_str = "Bootloader" - elif args.target == "a": - context_str = "App Slot A" - elif args.target == "b": - context_str = "App Slot B" - _LOGGER.info( - f"Flashing {context_str} with image {file_path} (size {total_size})" - ) - for idx, segment in enumerate(loadable_segments): - _LOGGER.info( - f"Loadable section {idx} {segment.name} with offset {segment.offset:#08x} and size {segment.size}" - ) - for segment in loadable_segments: - segment_end = segment.offset + segment.size - current_addr = segment.offset - pos_in_segment = 0 - while pos_in_segment < segment.size: - next_chunk_size = min(segment_end - current_addr, CHUNK_SIZE) - data = segment.data[ - pos_in_segment : pos_in_segment + next_chunk_size - ] - next_packet = pack_memory_write_command(current_addr, data) - _LOGGER.info( - f"Sending memory write command for address {current_addr:#08x} and data with " - f"length {len(data)}" - ) - verificator.add_tc(next_packet) - com_if.send(next_packet.pack()) - current_addr += next_chunk_size - pos_in_segment += next_chunk_size - while True: - data_available = com_if.data_available(0.1) - done = False - if not data_available: - continue - replies = com_if.receive() - for reply in replies: - tm = PusTm.unpack(reply, 0) - if tm.service != 1: - continue - service_1_tm = Service1Tm.from_tm(tm, UnpackParams(0)) - check_result = verificator.add_tm(service_1_tm) - # We could send after we have received the step reply, but that can - # somehow lead to overrun errors. I think it's okay to do it like - # this as long as the flash loader only uses polling.. - if ( - check_result is not None - and check_result.status.completed == StatusField.SUCCESS - ): - done = True - # Still keep a small delay - # time.sleep(0.05) - verificator.remove_completed_entries() - if done: - break - if args.target == "bl": - _LOGGER.info("Blanking the bootloader checksum") - # Blank the checksum. For the bootloader, the bootloader will calculate the - # checksum itself on the initial run. - checksum_write_packet = pack_memory_write_command( - BOOTLOADER_CRC_ADDR, bytes([0x00, 0x00, 0x00, 0x00]) - ) - com_if.send(checksum_write_packet.pack()) - else: - crc_addr = None - size_addr = None - if args.target == "a": - crc_addr = APP_A_CRC_ADDR - size_addr = APP_A_SIZE_ADDR - elif args.target == "b": - crc_addr = APP_B_CRC_ADDR - size_addr = APP_B_SIZE_ADDR - assert crc_addr is not None - assert size_addr is not None - _LOGGER.info( - f"Writing app size {total_size} at address {size_addr:#08x}" - ) - size_write_packet = pack_memory_write_command( - size_addr, struct.pack("!I", total_size) - ) - com_if.send(size_write_packet.pack()) - time.sleep(0.2) - crc_calc = PredefinedCrc("crc-32") - for segment in loadable_segments: - crc_calc.update(segment.data) - checksum = crc_calc.digest() - _LOGGER.info( - f"Writing checksum 0x[{checksum.hex(sep=',')}] at address {crc_addr:#08x}" - ) - checksum_write_packet = pack_memory_write_command(crc_addr, checksum) - com_if.send(checksum_write_packet.pack()) com_if.close() - return 0 + return result + + +def create_loadable_segments( + target: Target, file_path: Path +) -> Tuple[List[LoadableSegment], int]: + loadable_segments = [] + total_size = 0 + with open(file_path, "rb") as app_file: + elf_file = ELFFile(app_file) + + for idx, segment in enumerate(elf_file.iter_segments("PT_LOAD")): + if segment.header.p_filesz == 0: + continue + # Basic validity checks of the base addresses. + if idx == 0: + if ( + target == Target.BOOTLOADER + and segment.header.p_paddr != BOOTLOADER_START_ADDR + ): + raise ValueError( + f"detected possibly invalid start address {segment.header.p_paddr:#08x} for " + f"bootloader, expected {BOOTLOADER_START_ADDR}" + ) + if ( + target == Target.APP_A + and segment.header.p_paddr != APP_A_START_ADDR + ): + raise ValueError( + f"detected possibly invalid start address {segment.header.p_paddr:#08x} for " + f"App A, expected {APP_A_START_ADDR}" + ) + if ( + target == Target.APP_B + and segment.header.p_paddr != APP_B_START_ADDR + ): + raise ValueError( + f"detected possibly invalid start address {segment.header.p_paddr:#08x} for " + f"App B, expected {APP_B_START_ADDR}" + ) + name = None + for section in elf_file.iter_sections(): + if ( + section.header.sh_offset == segment.header.p_offset + and section.header.sh_size > 0 + ): + name = section.name + if name is None: + _LOGGER.warning("no fitting section found for segment") + continue + # print(f"Segment Addr: {segment.header.p_paddr}") + # print(f"Segment Offset: {segment.header.p_offset}") + # print(f"Segment Filesize: {segment.header.p_filesz}") + loadable_segments.append( + LoadableSegment( + name=name, + offset=segment.header.p_paddr, + size=segment.header.p_filesz, + data=segment.data(), + ) + ) + total_size += segment.header.p_filesz + return loadable_segments, total_size + + +def segments_info_str( + target: Target, + loadable_segments: List[LoadableSegment], + total_size: int, + file_path: Path, +): + # Set context string and perform basic sanity checks. + if target == Target.BOOTLOADER: + if total_size > BOOTLOADER_MAX_SIZE: + _LOGGER.error( + f"provided bootloader app larger than allowed {total_size} bytes" + ) + return -1 + context_str = "Bootloader" + elif target == Target.APP_A: + if total_size > APP_A_MAX_SIZE: + _LOGGER.error(f"provided App A larger than allowed {total_size} bytes") + return -1 + context_str = "App Slot A" + elif target == Target.APP_B: + if total_size > APP_B_MAX_SIZE: + _LOGGER.error(f"provided App B larger than allowed {total_size} bytes") + return -1 + context_str = "App Slot B" + _LOGGER.info(f"Flashing {context_str} with image {file_path} (size {total_size})") + for idx, segment in enumerate(loadable_segments): + _LOGGER.info( + f"Loadable section {idx} {segment.name} with offset {segment.offset:#08x} and " + f"size {segment.size}" + ) def pack_memory_write_command(addr: int, data: bytes) -> PusTc: @@ -324,7 +422,7 @@ def pack_memory_write_command(addr: int, data: bytes) -> PusTc: service=MEMORY_SERVICE, subservice=RAW_MEMORY_WRITE_SUBSERVICE, seq_count=SEQ_PROVIDER.get_and_increment(), - app_data=app_data, + app_data=bytes(app_data), ) diff --git a/flashloader/src/main.rs b/flashloader/src/main.rs index 98edfca..68210dd 100644 --- a/flashloader/src/main.rs +++ b/flashloader/src/main.rs @@ -109,6 +109,7 @@ mod app { tc::PusTcReader, tm::PusTmCreator, EcssEnumU8, PusPacket, WritablePusPacket, }; use va416xx_hal::irq_router::enable_and_init_irq_router; + use va416xx_hal::uart::IrqContextTimeoutOrMaxSize; use va416xx_hal::{ clock::ClkgenExt, edac, @@ -132,6 +133,7 @@ mod app { struct Local { uart_rx: uart::RxWithIrq, uart_tx: uart::Tx, + rx_context: IrqContextTimeoutOrMaxSize, rom_spi: Option, // We handle all TM in one task. tm_cons: DataConsumer, @@ -178,7 +180,7 @@ mod app { &mut cx.device.sysconfig, &clocks, ); - let (tx, mut rx, _) = uart0.split_with_irq(); + let (tx, rx) = uart0.split(); let verif_reporter = VerificationReportCreator::new(0).unwrap(); @@ -191,7 +193,9 @@ mod app { Mono::start(cx.core.SYST, clocks.sysclk().raw()); CLOCKS.set(clocks).unwrap(); - rx.read_fixed_len_using_irq(MAX_TC_FRAME_SIZE, true) + let mut rx = rx.to_rx_with_irq(); + let mut rx_context = IrqContextTimeoutOrMaxSize::new(MAX_TC_FRAME_SIZE); + rx.read_fixed_len_or_timeout_based_using_irq(&mut rx_context) .expect("initiating UART RX failed"); pus_tc_handler::spawn().unwrap(); pus_tm_tx_handler::spawn().unwrap(); @@ -205,6 +209,7 @@ mod app { Local { uart_rx: rx, uart_tx: tx, + rx_context, rom_spi: Some(cx.device.spi3), tm_cons: DataConsumer { buf_cons: buf_cons_tm, @@ -231,20 +236,26 @@ mod app { } } + // This is the interrupt handler to read all bytes received on the UART0. #[task( binds = UART0_RX, local = [ cnt: u32 = 0, rx_buf: [u8; MAX_TC_FRAME_SIZE] = [0; MAX_TC_FRAME_SIZE], + rx_context, uart_rx, tc_prod ], )] fn uart_rx_irq(cx: uart_rx_irq::Context) { - match cx.local.uart_rx.irq_handler(cx.local.rx_buf) { + match cx + .local + .uart_rx + .irq_handler_max_size_or_timeout_based(cx.local.rx_context, cx.local.rx_buf) + { Ok(result) => { if RX_DEBUGGING { - log::debug!("RX Info: {:?}", cx.local.uart_rx.irq_info()); + log::debug!("RX Info: {:?}", cx.local.rx_context); log::debug!("RX Result: {:?}", result); } if result.complete() { @@ -279,7 +290,7 @@ mod app { // Initiate next transfer. cx.local .uart_rx - .read_fixed_len_using_irq(MAX_TC_FRAME_SIZE, true) + .read_fixed_len_or_timeout_based_using_irq(cx.local.rx_context) .expect("read operation failed"); } if result.error() { @@ -438,7 +449,12 @@ mod app { return; } let data = &app_data[10..10 + data_len as usize]; - log::info!("writing {} bytes at offset {} to NVM", data_len, offset); + log::info!( + target: "TC Handler", + "writing {} bytes at offset {} to NVM", + data_len, + offset + ); // Safety: We only use this for NVM handling and we only do NVM // handling here. let mut sys_cfg = unsafe { pac::Sysconfig::steal() }; @@ -455,7 +471,9 @@ mod app { .completion_success(cx.local.src_data_buf, started_token, 0, 0, &[]) .expect("completion success failed"); write_and_send(&tm); - log::info!("NVM operation done"); + log::info!( + target: "TC Handler", + "NVM operation done"); } } } diff --git a/va416xx-hal/CHANGELOG.md b/va416xx-hal/CHANGELOG.md index fdcdbb0..42dce3f 100644 --- a/va416xx-hal/CHANGELOG.md +++ b/va416xx-hal/CHANGELOG.md @@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Improve and fix SPI abstractions. Add new low level interface. The primary SPI constructor now only expects a configuration structure and the transfer configuration needs to be applied in a separate step. +- Added an additional way to read the UART RX with IRQs. The module documentation provides + more information. +- Made the UART with IRQ API more flexible for future additions. ## Fixed diff --git a/va416xx-hal/src/uart.rs b/va416xx-hal/src/uart.rs index aa7b6d1..859aa5c 100644 --- a/va416xx-hal/src/uart.rs +++ b/va416xx-hal/src/uart.rs @@ -1,5 +1,9 @@ //! # API for the UART peripheral //! +//! The core of this API are the [Uart], [UartBase], [Rx] and [Tx] structures. +//! The RX structure also has a dedicated [RxWithIrq] variant which allows reading the receiver +//! using interrupts. +//! //! ## Examples //! //! - [UART simple example](https://egit.irs.uni-stuttgart.de/rust/va416xx-rs/src/branch/main/examples/simple/examples/uart.rs) @@ -198,25 +202,49 @@ impl From for Config { // IRQ Definitions //================================================================================================== -#[derive(Debug)] -pub struct IrqInfo { - rx_len: usize, +#[derive(Debug, Copy, Clone)] +pub struct IrqContextTimeoutOrMaxSize { rx_idx: usize, mode: IrqReceptionMode, + pub max_len: usize, +} + +impl IrqContextTimeoutOrMaxSize { + pub fn new(max_len: usize) -> Self { + IrqContextTimeoutOrMaxSize { + rx_idx: 0, + max_len, + mode: IrqReceptionMode::Idle, + } + } +} + +impl IrqContextTimeoutOrMaxSize { + pub fn reset(&mut self) { + self.rx_idx = 0; + self.mode = IrqReceptionMode::Idle; + } } /// This struct is used to return the default IRQ handler result to the user #[derive(Debug, Default)] pub struct IrqResult { + pub bytes_read: usize, + pub errors: IrqUartError, +} + +/// This struct is used to return the default IRQ handler result to the user +#[derive(Debug, Default)] +pub struct IrqResultMaxSizeTimeout { complete: bool, timeout: bool, pub errors: IrqUartError, pub bytes_read: usize, } -impl IrqResult { +impl IrqResultMaxSizeTimeout { pub fn new() -> Self { - IrqResult { + IrqResultMaxSizeTimeout { complete: false, timeout: false, errors: IrqUartError::default(), @@ -224,7 +252,7 @@ impl IrqResult { } } } -impl IrqResult { +impl IrqResultMaxSizeTimeout { #[inline] pub fn error(&self) -> bool { if self.errors.overflow || self.errors.parity || self.errors.framing { @@ -259,7 +287,7 @@ impl IrqResult { } } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Copy, Clone)] enum IrqReceptionMode { Idle, Pending, @@ -281,16 +309,14 @@ pub struct Uart { pins: Pins, } -/// Serial receiver +/// Serial receiver. +/// +/// Can be created by using the [Uart::split] or [UartBase::split] API. pub struct Rx(Uart); -// Serial receiver, using interrupts to offload reading to the hardware. -pub struct RxWithIrq { - inner: Rx, - irq_info: IrqInfo, -} - /// Serial transmitter +/// +/// Can be created by using the [Uart::split] or [UartBase::split] API. pub struct Tx(Uart); impl Rx { @@ -551,33 +577,6 @@ impl, RxPinInst: RxPin, UartInstanc self } - /// If the IRQ capabilities of the peripheral are used, the UART needs to be converted - /// with this function. Currently, IRQ abstractions are only implemented for the RX part - /// of the UART, so this function will release a TX and RX handle as well as the pin - /// instances. - pub fn split_with_irq( - self, - ) -> ( - Tx, - RxWithIrq, - (TxPinInst, RxPinInst), - ) { - let (inner, pins) = self.downgrade_internal(); - let (tx, rx) = inner.split(); - ( - tx, - RxWithIrq { - inner: rx, - irq_info: IrqInfo { - rx_len: 0, - rx_idx: 0, - mode: IrqReceptionMode::Idle, - }, - }, - pins, - ) - } - delegate::delegate! { to self.inner { #[inline] @@ -604,15 +603,6 @@ impl, RxPinInst: RxPin, UartInstanc } } - fn downgrade_internal(self) -> (UartBase, (TxPinInst, RxPinInst)) { - let base = UartBase { - uart: self.inner.uart, - tx: self.inner.tx, - rx: self.inner.rx, - }; - (base, self.pins) - } - pub fn downgrade(self) -> UartBase { UartBase { uart: self.inner.uart, @@ -651,6 +641,10 @@ impl Rx { self.0.enable().modify(|_, w| w.rxenable().clear_bit()); } + pub fn to_rx_with_irq(self) -> RxWithIrq { + RxWithIrq(self) + } + pub fn release(self) -> Uart { self.0 } @@ -691,6 +685,29 @@ pub struct IrqUartError { } impl IrqUartError { + #[inline(always)] + pub fn overflow(&self) -> bool { + self.overflow + } + + #[inline(always)] + pub fn framing(&self) -> bool { + self.framing + } + + #[inline(always)] + pub fn parity(&self) -> bool { + self.parity + } + + #[inline(always)] + pub fn other(&self) -> bool { + self.other + } +} + +impl IrqUartError { + #[inline(always)] pub fn error(&self) -> bool { self.overflow || self.framing || self.parity } @@ -702,35 +719,62 @@ pub enum IrqError { Uart(IrqUartError), } +/// Serial receiver, using interrupts to offload reading to the hardware. +/// +/// You can use [Rx::to_rx_with_irq] to convert a normal [Rx] structure into this structure. +/// This structure provides two distinct ways to read the UART RX using interrupts. It should +/// be noted that the interrupt service routine (ISR) still has to be provided by the user. However, +/// this structure provides API calls which can be used inside the ISRs to simplify the reading +/// of the UART. +/// +/// 1. The first way simply empties the FIFO on an interrupt into a user provided buffer. You +/// can simply use [Self::start] to prepare the peripheral and then call the +/// [Self::irq_handler] in the interrupt service routine. +/// 2. The second way reads packets bounded by a maximum size or a baudtick based timeout. You +/// can use [Self::read_fixed_len_or_timeout_based_using_irq] to prepare the peripheral and +/// then call the [Self::irq_handler_max_size_or_timeout_based] in the interrupt service +/// routine. You have to call [Self::read_fixed_len_or_timeout_based_using_irq] in the ISR to +/// start reading the next packet. +pub struct RxWithIrq(Rx); + impl RxWithIrq { - /// This initializes a non-blocking read transfer using the IRQ capabilities of the UART - /// peripheral. + /// This function should be called once at initialization time if the regular + /// [Self::irq_handler] is used to read the UART receiver to enable and start the receiver. + pub fn start(&mut self) { + self.0.enable(); + self.enable_rx_irq_sources(true); + unsafe { enable_interrupt(Uart::IRQ_RX) }; + } + + #[inline(always)] + pub fn uart(&self) -> &Uart { + &self.0 .0 + } + + /// This function is used together with the [Self::irq_handler_max_size_or_timeout_based] + /// function to read packets with a maximum size or variable sized packets by using the + /// receive timeout of the hardware. /// - /// The only required information is the maximum length for variable sized reception - /// or the expected length for fixed length reception. If variable sized packets are expected, - /// the timeout functionality of the IRQ should be enabled as well. After calling this function, - /// the [`irq_handler`](Self::irq_handler) function should be called in the user interrupt - /// handler to read the received packets and reinitiate another transfer if desired. - pub fn read_fixed_len_using_irq( + /// This function should be called once at initialization to initiate the context state + /// and to [Self::start] the receiver. After that, it should be called after each + /// completed [Self::irq_handler_max_size_or_timeout_based] call to restart the reception + /// of a packet. + pub fn read_fixed_len_or_timeout_based_using_irq( &mut self, - max_len: usize, - enb_timeout_irq: bool, + context: &mut IrqContextTimeoutOrMaxSize, ) -> Result<(), Error> { - if self.irq_info.mode != IrqReceptionMode::Idle { + if context.mode != IrqReceptionMode::Idle { return Err(Error::TransferPending); } - self.irq_info.mode = IrqReceptionMode::Pending; - self.irq_info.rx_idx = 0; - self.irq_info.rx_len = max_len; - self.inner.enable(); - self.enable_rx_irq_sources(enb_timeout_irq); - unsafe { enable_interrupt(Uart::IRQ_RX) }; + context.mode = IrqReceptionMode::Pending; + context.rx_idx = 0; + self.start(); Ok(()) } #[inline] fn enable_rx_irq_sources(&mut self, timeout: bool) { - self.inner.0.irq_enb().modify(|_, w| { + self.uart().irq_enb().modify(|_, w| { if timeout { w.irq_rx_to().set_bit(); } @@ -741,7 +785,7 @@ impl RxWithIrq { #[inline] fn disable_rx_irq_sources(&mut self) { - self.inner.0.irq_enb().modify(|_, w| { + self.uart().irq_enb().modify(|_, w| { w.irq_rx_to().clear_bit(); w.irq_rx_status().clear_bit(); w.irq_rx().clear_bit() @@ -750,30 +794,22 @@ impl RxWithIrq { pub fn cancel_transfer(&mut self) { self.disable_rx_irq_sources(); - self.inner.clear_fifo(); - self.irq_info.rx_idx = 0; - self.irq_info.rx_len = 0; + self.0.clear_fifo(); } - pub fn uart(&self) -> &Uart { - &self.inner.0 - } - - /// Default IRQ handler which can be used to read the packets arriving on the UART peripheral. + /// This function should be called in the user provided UART interrupt handler. /// - /// If passed buffer is equal to or larger than the specified maximum length, an - /// [`Error::BufferTooShort`] will be returned - pub fn irq_handler(&mut self, buf: &mut [u8]) -> Result { - if buf.len() < self.irq_info.rx_len { - return Err(IrqError::BufferTooShort { - found: buf.len(), - expected: self.irq_info.rx_len, - }); - } - let mut res = IrqResult::default(); + /// It simply empties any bytes in the FIFO into the user provided buffer and returns the + /// result of the operation. + /// + /// This function will not disable the RX interrupts, so you don't need to call any other + /// API after calling this function to continue emptying the FIFO. + pub fn irq_handler(&mut self, buf: &mut [u8; 16]) -> Result { + let mut result = IrqResult::default(); + let mut current_idx = 0; - let irq_end = self.inner.0.irq_end().read(); - let enb_status = self.inner.0.enable().read(); + let irq_end = self.uart().irq_end().read(); + let enb_status = self.uart().enable().read(); let rx_enabled = enb_status.rxenable().bit_is_set(); // Half-Full interrupt. We have a guaranteed amount of data we can read. @@ -782,18 +818,84 @@ impl RxWithIrq { // We use this trick/hack because the timeout feature of the peripheral relies on data // being in the RX FIFO. If data continues arriving, another half-full IRQ will fire. // If not, the last byte(s) is/are emptied by the timeout interrupt. - let available_bytes = self.inner.0.rxfifoirqtrg().read().bits() as usize; + let available_bytes = self.uart().rxfifoirqtrg().read().bits() as usize; + + // If this interrupt bit is set, the trigger level is available at the very least. + // Read everything as fast as possible + for _ in 0..available_bytes { + buf[current_idx] = (self.uart().data().read().bits() & 0xff) as u8; + current_idx += 1; + } + } + + // Timeout, empty the FIFO completely. + if irq_end.irq_rx_to().bit_is_set() { + let read_result = self.0.read(); + // While there is data in the FIFO, write it into the reception buffer + while let Some(byte) = self.read_handler(&mut result.errors, &read_result) { + buf[current_idx] = byte; + current_idx += 1; + } + } + + // RX transfer not complete, check for RX errors + if rx_enabled { + self.check_for_errors(&mut result.errors); + } + + // Clear the interrupt status bits + self.uart() + .irq_clr() + .write(|w| unsafe { w.bits(irq_end.bits()) }); + Ok(result) + } + + /// This function should be called in the user provided UART interrupt handler. + /// + /// This function is used to read packets which either have a maximum size or variable sized + /// packet which are bounded by sufficient delays between them, triggering a hardware timeout. + /// + /// If either the maximum number of packets have been read or a timeout occured, the transfer + /// will be deemed completed. The state information of the transfer is tracked in the + /// [IrqContextTimeoutOrMaxSize] structure. + /// + /// If passed buffer is equal to or larger than the specified maximum length, an + /// [`Error::BufferTooShort`] will be returned + pub fn irq_handler_max_size_or_timeout_based( + &mut self, + context: &mut IrqContextTimeoutOrMaxSize, + buf: &mut [u8], + ) -> Result { + if buf.len() < context.max_len { + return Err(IrqError::BufferTooShort { + found: buf.len(), + expected: context.max_len, + }); + } + let mut result = IrqResultMaxSizeTimeout::default(); + + let irq_end = self.uart().irq_end().read(); + let enb_status = self.uart().enable().read(); + let rx_enabled = enb_status.rxenable().bit_is_set(); + + // Half-Full interrupt. We have a guaranteed amount of data we can read. + if irq_end.irq_rx().bit_is_set() { + // Determine the number of bytes to read, ensuring we leave 1 byte in the FIFO. + // We use this trick/hack because the timeout feature of the peripheral relies on data + // being in the RX FIFO. If data continues arriving, another half-full IRQ will fire. + // If not, the last byte(s) is/are emptied by the timeout interrupt. + let available_bytes = self.uart().rxfifoirqtrg().read().bits() as usize; let bytes_to_read = core::cmp::min( available_bytes.saturating_sub(1), - self.irq_info.rx_len - self.irq_info.rx_idx, + context.max_len - context.rx_idx, ); // If this interrupt bit is set, the trigger level is available at the very least. // Read everything as fast as possible for _ in 0..bytes_to_read { - buf[self.irq_info.rx_idx] = (self.inner.0.data().read().bits() & 0xff) as u8; - self.irq_info.rx_idx += 1; + buf[context.rx_idx] = (self.uart().data().read().bits() & 0xff) as u8; + context.rx_idx += 1; } // On high-baudrates, data might be available immediately, and we possible have to @@ -801,93 +903,94 @@ impl RxWithIrq { // rely on the hardware firing another IRQ. I have not tried baudrates higher than // 115200 so far. } - let read_handler = - |possible_error: &mut IrqUartError, read_res: nb::Result| -> Option { - match read_res { - Ok(byte) => Some(byte), - Err(nb::Error::WouldBlock) => None, - Err(nb::Error::Other(e)) => { - match e { - Error::Overrun => { - possible_error.overflow = true; - } - Error::FramingError => { - possible_error.framing = true; - } - Error::ParityError => { - possible_error.parity = true; - } - _ => { - possible_error.other = true; - } - } - None - } - } - }; // Timeout, empty the FIFO completely. if irq_end.irq_rx_to().bit_is_set() { // While there is data in the FIFO, write it into the reception buffer loop { - if self.irq_info.rx_idx == self.irq_info.rx_len { + if context.rx_idx == context.max_len { break; } - if let Some(byte) = read_handler(&mut res.errors, self.inner.read()) { - buf[self.irq_info.rx_idx] = byte; - self.irq_info.rx_idx += 1; + let read_result = self.0.read(); + if let Some(byte) = self.read_handler(&mut result.errors, &read_result) { + buf[context.rx_idx] = byte; + context.rx_idx += 1; } else { break; } } - self.irq_completion_handler(&mut res); - return Ok(res); + self.irq_completion_handler_max_size_timeout(&mut result, context); + return Ok(result); } // RX transfer not complete, check for RX errors - if (self.irq_info.rx_idx < self.irq_info.rx_len) && rx_enabled { - // Read status register again, might have changed since reading received data - let rx_status = self.inner.0.rxstatus().read(); - if rx_status.rxovr().bit_is_set() { - res.errors.overflow = true; - } - if rx_status.rxfrm().bit_is_set() { - res.errors.framing = true; - } - if rx_status.rxpar().bit_is_set() { - res.errors.parity = true; - } - - // If it is not a timeout, it's an error - if res.error() { - self.disable_rx_irq_sources(); - return Err(IrqError::Uart(res.errors)); - } + if (context.rx_idx < context.max_len) && rx_enabled { + self.check_for_errors(&mut result.errors); } // Clear the interrupt status bits - self.inner - .0 + self.uart() .irq_clr() .write(|w| unsafe { w.bits(irq_end.bits()) }); - Ok(res) + Ok(result) } - fn irq_completion_handler(&mut self, res: &mut IrqResult) { + fn read_handler( + &self, + errors: &mut IrqUartError, + read_res: &nb::Result, + ) -> Option { + match read_res { + Ok(byte) => Some(*byte), + Err(nb::Error::WouldBlock) => None, + Err(nb::Error::Other(e)) => { + match e { + Error::Overrun => { + errors.overflow = true; + } + Error::FramingError => { + errors.framing = true; + } + Error::ParityError => { + errors.parity = true; + } + _ => { + errors.other = true; + } + } + None + } + } + } + + fn check_for_errors(&self, errors: &mut IrqUartError) { + // Read status register again, might have changed since reading received data + let rx_status = self.uart().rxstatus().read(); + if rx_status.rxovr().bit_is_set() { + errors.overflow = true; + } + if rx_status.rxfrm().bit_is_set() { + errors.framing = true; + } + if rx_status.rxpar().bit_is_set() { + errors.parity = true; + } + } + + fn irq_completion_handler_max_size_timeout( + &mut self, + res: &mut IrqResultMaxSizeTimeout, + context: &mut IrqContextTimeoutOrMaxSize, + ) { self.disable_rx_irq_sources(); - self.inner.disable(); - res.bytes_read = self.irq_info.rx_idx; + self.0.disable(); + res.bytes_read = context.rx_idx; res.complete = true; - self.irq_info.mode = IrqReceptionMode::Idle; - self.irq_info.rx_idx = 0; - self.irq_info.rx_len = 0; - } - - pub fn irq_info(&self) -> &IrqInfo { - &self.irq_info + context.mode = IrqReceptionMode::Idle; + context.rx_idx = 0; } pub fn release(self) -> Uart { - self.inner.release() + self.0.release() } }