Flashloader and UART update #31

Merged
muellerr merged 1 commits from flashloader-and-uart-update into main 2024-09-23 12:00:24 +02:00
6 changed files with 608 additions and 371 deletions

View File

@ -45,17 +45,26 @@ const BOOTLOADER_START_ADDR: u32 = 0x0;
const BOOTLOADER_CRC_ADDR: u32 = BOOTLOADER_END_ADDR - 4; const BOOTLOADER_CRC_ADDR: u32 = BOOTLOADER_END_ADDR - 4;
const BOOTLOADER_END_ADDR: u32 = 0x4000; const BOOTLOADER_END_ADDR: u32 = 0x4000;
const APP_A_START_ADDR: u32 = BOOTLOADER_END_ADDR; // 0x4000 // 0x4000
// The actual size of the image which is relevant for CRC calculation. const APP_A_START_ADDR: u32 = BOOTLOADER_END_ADDR;
const APP_A_SIZE_ADDR: u32 = APP_B_END_ADDR - 8; // 0x21FF8 // The actual size of the image which is relevant for CRC calculation will be store at this
const APP_A_CRC_ADDR: u32 = APP_B_END_ADDR - 4; // 0x21FFC // 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; 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 // 0x22000
// The actual size of the image which is relevant for CRC calculation. const APP_B_START_ADDR: u32 = APP_A_END_ADDR;
const APP_B_SIZE_ADDR: u32 = APP_B_END_ADDR - 8; // 0x3FFF8 // The actual size of the image which is relevant for CRC calculation will be stored at this
const APP_B_CRC_ADDR: u32 = APP_B_END_ADDR - 4; // 0x3FFFC // address.
pub const APP_B_END_ADDR: u32 = NVM_SIZE; // 0x40000 // 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; pub const APP_IMG_SZ: u32 = APP_B_END_ADDR - APP_A_START_ADDR / 2;

View File

@ -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 called the `image-loader.py` which can be used to upload compiled images to the flashloader
application to write them to the NVM. 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 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 the Python script provided here to upload images because it uses a low-level CCSDS based packet
interface. interface.
## Using the Python image loader ## 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 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 systems you can use `python3 -m venv venv` and then `source venv/bin/activate` to create
and activate a virtual environment. and activate a virtual environment.

View File

@ -1,6 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
from typing import List, Tuple
from spacepackets.ecss.defs import PusService from spacepackets.ecss.defs import PusService
from spacepackets.ecss.tm import PusTm from spacepackets.ecss.tm import PusTm
from tmtccmd.com import ComInterface
import toml import toml
import struct import struct
import logging import logging
@ -21,20 +23,27 @@ from elftools.elf.elffile import ELFFile
BAUD_RATE = 115200 BAUD_RATE = 115200
BOOTLOADER_START_ADDR = 0x0 BOOTLOADER_START_ADDR = 0x0
BOOTLOADER_END_ADDR = 0x4000 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_START_ADDR = 0x4000
APP_A_END_ADDR = 0x22000 APP_A_END_ADDR = 0x22000
# The actual size of the image which is relevant for CRC calculation. # The actual size of the image which is relevant for CRC calculation.
APP_A_SIZE_ADDR = 0x21FF8 APP_A_SIZE_ADDR = APP_A_END_ADDR - 8
APP_A_CRC_ADDR = 0x21FFC 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_START_ADDR = 0x22000
APP_B_END_ADDR = 0x40000 APP_B_END_ADDR = 0x40000
# The actual size of the image which is relevant for CRC calculation. # The actual size of the image which is relevant for CRC calculation.
APP_B_SIZE_ADDR = 0x3FFF8 APP_B_SIZE_ADDR = APP_B_END_ADDR - 8
APP_B_CRC_ADDR = 0x3FFFC APP_B_CRC_ADDR = APP_B_END_ADDR - 4
APP_IMG_SZ = 0x1E000 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 CHUNK_SIZE = 896
@ -52,6 +61,7 @@ class ActionId(enum.IntEnum):
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SEQ_PROVIDER = SeqCountProvider(bit_width=14)
@dataclasses.dataclass @dataclasses.dataclass
@ -62,7 +72,174 @@ class LoadableSegment:
data: bytes 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: def main() -> int:
@ -102,74 +279,57 @@ def main() -> int:
verificator = PusVerificator() verificator = PusVerificator()
com_if = SerialCobsComIF(serial_cfg) com_if = SerialCobsComIF(serial_cfg)
com_if.open() 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 file_path = None
result = -1
if args.ping: if args.ping:
_LOGGER.info("Sending ping command") image_loader.handle_ping_cmd()
ping_tc = PusTc( com_if.close()
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 return 0
if args.target: if target:
if not args.corrupt: if not args.corrupt:
if not args.path: if not args.path:
_LOGGER.error("App Path needs to be specified for the flash process") _LOGGER.error("App Path needs to be specified for the flash process")
return -1
file_path = Path(args.path) file_path = Path(args.path)
if not file_path.exists(): if not file_path.exists():
_LOGGER.error("File does not exist") _LOGGER.error("File does not exist")
return -1
if args.corrupt: if args.corrupt:
if not args.target: if not target:
_LOGGER.error("target for corruption command required") _LOGGER.error("target for corruption command required")
com_if.close()
return -1 return -1
if args.target == "bl": image_loader.handle_corruption_cmd(target)
_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())
else: else:
assert file_path is not None assert file_path is not None
assert target is not None
result = image_loader.handle_flash_cmd(target, file_path)
com_if.close()
return result
def create_loadable_segments(
target: Target, file_path: Path
) -> Tuple[List[LoadableSegment], int]:
loadable_segments = [] loadable_segments = []
_LOGGER.info("Parsing ELF file for loadable sections")
total_size = 0 total_size = 0
with open(file_path, "rb") as app_file: with open(file_path, "rb") as app_file:
elf_file = ELFFile(app_file) elf_file = ELFFile(app_file)
for (idx, segment) in enumerate(elf_file.iter_segments("PT_LOAD")): for idx, segment in enumerate(elf_file.iter_segments("PT_LOAD")):
if segment.header.p_filesz == 0: if segment.header.p_filesz == 0:
continue continue
# Basic validity checks of the base addresses. # Basic validity checks of the base addresses.
if idx == 0: if idx == 0:
if ( if (
args.target == "bl" target == Target.BOOTLOADER
and segment.header.p_paddr != BOOTLOADER_START_ADDR and segment.header.p_paddr != BOOTLOADER_START_ADDR
): ):
raise ValueError( raise ValueError(
@ -177,7 +337,7 @@ def main() -> int:
f"bootloader, expected {BOOTLOADER_START_ADDR}" f"bootloader, expected {BOOTLOADER_START_ADDR}"
) )
if ( if (
args.target == "a" target == Target.APP_A
and segment.header.p_paddr != APP_A_START_ADDR and segment.header.p_paddr != APP_A_START_ADDR
): ):
raise ValueError( raise ValueError(
@ -185,7 +345,7 @@ def main() -> int:
f"App A, expected {APP_A_START_ADDR}" f"App A, expected {APP_A_START_ADDR}"
) )
if ( if (
args.target == "b" target == Target.APP_B
and segment.header.p_paddr != APP_B_START_ADDR and segment.header.p_paddr != APP_B_START_ADDR
): ):
raise ValueError( raise ValueError(
@ -214,101 +374,39 @@ def main() -> int:
) )
) )
total_size += segment.header.p_filesz total_size += segment.header.p_filesz
context_str = None return loadable_segments, total_size
if args.target == "bl":
context_str = "Bootloader"
elif args.target == "a": def segments_info_str(
context_str = "App Slot A" target: Target,
elif args.target == "b": loadable_segments: List[LoadableSegment],
context_str = "App Slot B" total_size: int,
_LOGGER.info( file_path: Path,
f"Flashing {context_str} with image {file_path} (size {total_size})" ):
# 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): for idx, segment in enumerate(loadable_segments):
_LOGGER.info( _LOGGER.info(
f"Loadable section {idx} {segment.name} with offset {segment.offset:#08x} and size {segment.size}" f"Loadable section {idx} {segment.name} with offset {segment.offset:#08x} and "
f"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
def pack_memory_write_command(addr: int, data: bytes) -> PusTc: 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, service=MEMORY_SERVICE,
subservice=RAW_MEMORY_WRITE_SUBSERVICE, subservice=RAW_MEMORY_WRITE_SUBSERVICE,
seq_count=SEQ_PROVIDER.get_and_increment(), seq_count=SEQ_PROVIDER.get_and_increment(),
app_data=app_data, app_data=bytes(app_data),
) )

View File

@ -109,6 +109,7 @@ mod app {
tc::PusTcReader, tm::PusTmCreator, EcssEnumU8, PusPacket, WritablePusPacket, tc::PusTcReader, tm::PusTmCreator, EcssEnumU8, PusPacket, WritablePusPacket,
}; };
use va416xx_hal::irq_router::enable_and_init_irq_router; use va416xx_hal::irq_router::enable_and_init_irq_router;
use va416xx_hal::uart::IrqContextTimeoutOrMaxSize;
use va416xx_hal::{ use va416xx_hal::{
clock::ClkgenExt, clock::ClkgenExt,
edac, edac,
@ -132,6 +133,7 @@ mod app {
struct Local { struct Local {
uart_rx: uart::RxWithIrq<pac::Uart0>, uart_rx: uart::RxWithIrq<pac::Uart0>,
uart_tx: uart::Tx<pac::Uart0>, uart_tx: uart::Tx<pac::Uart0>,
rx_context: IrqContextTimeoutOrMaxSize,
rom_spi: Option<pac::Spi3>, rom_spi: Option<pac::Spi3>,
// We handle all TM in one task. // We handle all TM in one task.
tm_cons: DataConsumer<BUF_RB_SIZE_TM, SIZES_RB_SIZE_TM>, tm_cons: DataConsumer<BUF_RB_SIZE_TM, SIZES_RB_SIZE_TM>,
@ -178,7 +180,7 @@ mod app {
&mut cx.device.sysconfig, &mut cx.device.sysconfig,
&clocks, &clocks,
); );
let (tx, mut rx, _) = uart0.split_with_irq(); let (tx, rx) = uart0.split();
let verif_reporter = VerificationReportCreator::new(0).unwrap(); let verif_reporter = VerificationReportCreator::new(0).unwrap();
@ -191,7 +193,9 @@ mod app {
Mono::start(cx.core.SYST, clocks.sysclk().raw()); Mono::start(cx.core.SYST, clocks.sysclk().raw());
CLOCKS.set(clocks).unwrap(); 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"); .expect("initiating UART RX failed");
pus_tc_handler::spawn().unwrap(); pus_tc_handler::spawn().unwrap();
pus_tm_tx_handler::spawn().unwrap(); pus_tm_tx_handler::spawn().unwrap();
@ -205,6 +209,7 @@ mod app {
Local { Local {
uart_rx: rx, uart_rx: rx,
uart_tx: tx, uart_tx: tx,
rx_context,
rom_spi: Some(cx.device.spi3), rom_spi: Some(cx.device.spi3),
tm_cons: DataConsumer { tm_cons: DataConsumer {
buf_cons: buf_cons_tm, 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( #[task(
binds = UART0_RX, binds = UART0_RX,
local = [ local = [
cnt: u32 = 0, cnt: u32 = 0,
rx_buf: [u8; MAX_TC_FRAME_SIZE] = [0; MAX_TC_FRAME_SIZE], rx_buf: [u8; MAX_TC_FRAME_SIZE] = [0; MAX_TC_FRAME_SIZE],
rx_context,
uart_rx, uart_rx,
tc_prod tc_prod
], ],
)] )]
fn uart_rx_irq(cx: uart_rx_irq::Context) { 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) => { Ok(result) => {
if RX_DEBUGGING { 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); log::debug!("RX Result: {:?}", result);
} }
if result.complete() { if result.complete() {
@ -279,7 +290,7 @@ mod app {
// Initiate next transfer. // Initiate next transfer.
cx.local cx.local
.uart_rx .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"); .expect("read operation failed");
} }
if result.error() { if result.error() {
@ -438,7 +449,12 @@ mod app {
return; return;
} }
let data = &app_data[10..10 + data_len as usize]; 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 // Safety: We only use this for NVM handling and we only do NVM
// handling here. // handling here.
let mut sys_cfg = unsafe { pac::Sysconfig::steal() }; 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, &[]) .completion_success(cx.local.src_data_buf, started_token, 0, 0, &[])
.expect("completion success failed"); .expect("completion success failed");
write_and_send(&tm); write_and_send(&tm);
log::info!("NVM operation done"); log::info!(
target: "TC Handler",
"NVM operation done");
} }
} }
} }

View File

@ -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 - 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 only expects a configuration structure and the transfer configuration needs to be applied in a
separate step. 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 ## Fixed

View File

@ -1,5 +1,9 @@
//! # API for the UART peripheral //! # 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 //! ## Examples
//! //!
//! - [UART simple example](https://egit.irs.uni-stuttgart.de/rust/va416xx-rs/src/branch/main/examples/simple/examples/uart.rs) //! - [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<Hertz> for Config {
// IRQ Definitions // IRQ Definitions
//================================================================================================== //==================================================================================================
#[derive(Debug)] #[derive(Debug, Copy, Clone)]
pub struct IrqInfo { pub struct IrqContextTimeoutOrMaxSize {
rx_len: usize,
rx_idx: usize, rx_idx: usize,
mode: IrqReceptionMode, 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 /// This struct is used to return the default IRQ handler result to the user
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct IrqResult { 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, complete: bool,
timeout: bool, timeout: bool,
pub errors: IrqUartError, pub errors: IrqUartError,
pub bytes_read: usize, pub bytes_read: usize,
} }
impl IrqResult { impl IrqResultMaxSizeTimeout {
pub fn new() -> Self { pub fn new() -> Self {
IrqResult { IrqResultMaxSizeTimeout {
complete: false, complete: false,
timeout: false, timeout: false,
errors: IrqUartError::default(), errors: IrqUartError::default(),
@ -224,7 +252,7 @@ impl IrqResult {
} }
} }
} }
impl IrqResult { impl IrqResultMaxSizeTimeout {
#[inline] #[inline]
pub fn error(&self) -> bool { pub fn error(&self) -> bool {
if self.errors.overflow || self.errors.parity || self.errors.framing { 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 { enum IrqReceptionMode {
Idle, Idle,
Pending, Pending,
@ -281,16 +309,14 @@ pub struct Uart<UartInstance, Pins> {
pins: Pins, pins: Pins,
} }
/// Serial receiver /// Serial receiver.
///
/// Can be created by using the [Uart::split] or [UartBase::split] API.
pub struct Rx<Uart>(Uart); pub struct Rx<Uart>(Uart);
// Serial receiver, using interrupts to offload reading to the hardware.
pub struct RxWithIrq<Uart> {
inner: Rx<Uart>,
irq_info: IrqInfo,
}
/// Serial transmitter /// Serial transmitter
///
/// Can be created by using the [Uart::split] or [UartBase::split] API.
pub struct Tx<Uart>(Uart); pub struct Tx<Uart>(Uart);
impl<Uart: Instance> Rx<Uart> { impl<Uart: Instance> Rx<Uart> {
@ -551,33 +577,6 @@ impl<TxPinInst: TxPin<UartInstance>, RxPinInst: RxPin<UartInstance>, UartInstanc
self 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<UartInstance>,
RxWithIrq<UartInstance>,
(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! { delegate::delegate! {
to self.inner { to self.inner {
#[inline] #[inline]
@ -604,15 +603,6 @@ impl<TxPinInst: TxPin<UartInstance>, RxPinInst: RxPin<UartInstance>, UartInstanc
} }
} }
fn downgrade_internal(self) -> (UartBase<UartInstance>, (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<UartInstance> { pub fn downgrade(self) -> UartBase<UartInstance> {
UartBase { UartBase {
uart: self.inner.uart, uart: self.inner.uart,
@ -651,6 +641,10 @@ impl<Uart: Instance> Rx<Uart> {
self.0.enable().modify(|_, w| w.rxenable().clear_bit()); self.0.enable().modify(|_, w| w.rxenable().clear_bit());
} }
pub fn to_rx_with_irq(self) -> RxWithIrq<Uart> {
RxWithIrq(self)
}
pub fn release(self) -> Uart { pub fn release(self) -> Uart {
self.0 self.0
} }
@ -691,6 +685,29 @@ pub struct IrqUartError {
} }
impl 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 { pub fn error(&self) -> bool {
self.overflow || self.framing || self.parity self.overflow || self.framing || self.parity
} }
@ -702,35 +719,62 @@ pub enum IrqError {
Uart(IrqUartError), 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<Uart>(Rx<Uart>);
impl<Uart: Instance> RxWithIrq<Uart> { impl<Uart: Instance> RxWithIrq<Uart> {
/// This initializes a non-blocking read transfer using the IRQ capabilities of the UART /// This function should be called once at initialization time if the regular
/// peripheral. /// [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 /// This function should be called once at initialization to initiate the context state
/// or the expected length for fixed length reception. If variable sized packets are expected, /// and to [Self::start] the receiver. After that, it should be called after each
/// the timeout functionality of the IRQ should be enabled as well. After calling this function, /// completed [Self::irq_handler_max_size_or_timeout_based] call to restart the reception
/// the [`irq_handler`](Self::irq_handler) function should be called in the user interrupt /// of a packet.
/// handler to read the received packets and reinitiate another transfer if desired. pub fn read_fixed_len_or_timeout_based_using_irq(
pub fn read_fixed_len_using_irq(
&mut self, &mut self,
max_len: usize, context: &mut IrqContextTimeoutOrMaxSize,
enb_timeout_irq: bool,
) -> Result<(), Error> { ) -> Result<(), Error> {
if self.irq_info.mode != IrqReceptionMode::Idle { if context.mode != IrqReceptionMode::Idle {
return Err(Error::TransferPending); return Err(Error::TransferPending);
} }
self.irq_info.mode = IrqReceptionMode::Pending; context.mode = IrqReceptionMode::Pending;
self.irq_info.rx_idx = 0; context.rx_idx = 0;
self.irq_info.rx_len = max_len; self.start();
self.inner.enable();
self.enable_rx_irq_sources(enb_timeout_irq);
unsafe { enable_interrupt(Uart::IRQ_RX) };
Ok(()) Ok(())
} }
#[inline] #[inline]
fn enable_rx_irq_sources(&mut self, timeout: bool) { fn enable_rx_irq_sources(&mut self, timeout: bool) {
self.inner.0.irq_enb().modify(|_, w| { self.uart().irq_enb().modify(|_, w| {
if timeout { if timeout {
w.irq_rx_to().set_bit(); w.irq_rx_to().set_bit();
} }
@ -741,7 +785,7 @@ impl<Uart: Instance> RxWithIrq<Uart> {
#[inline] #[inline]
fn disable_rx_irq_sources(&mut self) { 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_to().clear_bit();
w.irq_rx_status().clear_bit(); w.irq_rx_status().clear_bit();
w.irq_rx().clear_bit() w.irq_rx().clear_bit()
@ -750,30 +794,22 @@ impl<Uart: Instance> RxWithIrq<Uart> {
pub fn cancel_transfer(&mut self) { pub fn cancel_transfer(&mut self) {
self.disable_rx_irq_sources(); self.disable_rx_irq_sources();
self.inner.clear_fifo(); self.0.clear_fifo();
self.irq_info.rx_idx = 0;
self.irq_info.rx_len = 0;
} }
pub fn uart(&self) -> &Uart { /// This function should be called in the user provided UART interrupt handler.
&self.inner.0
}
/// Default IRQ handler which can be used to read the packets arriving on the UART peripheral.
/// ///
/// If passed buffer is equal to or larger than the specified maximum length, an /// It simply empties any bytes in the FIFO into the user provided buffer and returns the
/// [`Error::BufferTooShort`] will be returned /// result of the operation.
pub fn irq_handler(&mut self, buf: &mut [u8]) -> Result<IrqResult, IrqError> { ///
if buf.len() < self.irq_info.rx_len { /// This function will not disable the RX interrupts, so you don't need to call any other
return Err(IrqError::BufferTooShort { /// API after calling this function to continue emptying the FIFO.
found: buf.len(), pub fn irq_handler(&mut self, buf: &mut [u8; 16]) -> Result<IrqResult, IrqUartError> {
expected: self.irq_info.rx_len, let mut result = IrqResult::default();
}); let mut current_idx = 0;
}
let mut res = IrqResult::default();
let irq_end = self.inner.0.irq_end().read(); let irq_end = self.uart().irq_end().read();
let enb_status = self.inner.0.enable().read(); let enb_status = self.uart().enable().read();
let rx_enabled = enb_status.rxenable().bit_is_set(); let rx_enabled = enb_status.rxenable().bit_is_set();
// Half-Full interrupt. We have a guaranteed amount of data we can read. // Half-Full interrupt. We have a guaranteed amount of data we can read.
@ -782,18 +818,84 @@ impl<Uart: Instance> RxWithIrq<Uart> {
// We use this trick/hack because the timeout feature of the peripheral relies on data // 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. // 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. // 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<IrqResultMaxSizeTimeout, IrqError> {
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( let bytes_to_read = core::cmp::min(
available_bytes.saturating_sub(1), 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. // If this interrupt bit is set, the trigger level is available at the very least.
// Read everything as fast as possible // Read everything as fast as possible
for _ in 0..bytes_to_read { for _ in 0..bytes_to_read {
buf[self.irq_info.rx_idx] = (self.inner.0.data().read().bits() & 0xff) as u8; buf[context.rx_idx] = (self.uart().data().read().bits() & 0xff) as u8;
self.irq_info.rx_idx += 1; context.rx_idx += 1;
} }
// On high-baudrates, data might be available immediately, and we possible have to // On high-baudrates, data might be available immediately, and we possible have to
@ -801,93 +903,94 @@ impl<Uart: Instance> RxWithIrq<Uart> {
// rely on the hardware firing another IRQ. I have not tried baudrates higher than // rely on the hardware firing another IRQ. I have not tried baudrates higher than
// 115200 so far. // 115200 so far.
} }
let read_handler = // Timeout, empty the FIFO completely.
|possible_error: &mut IrqUartError, read_res: nb::Result<u8, Error>| -> Option<u8> { if irq_end.irq_rx_to().bit_is_set() {
// While there is data in the FIFO, write it into the reception buffer
loop {
if context.rx_idx == context.max_len {
break;
}
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_max_size_timeout(&mut result, context);
return Ok(result);
}
// RX transfer not complete, check for RX errors
if (context.rx_idx < context.max_len) && 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)
}
fn read_handler(
&self,
errors: &mut IrqUartError,
read_res: &nb::Result<u8, Error>,
) -> Option<u8> {
match read_res { match read_res {
Ok(byte) => Some(byte), Ok(byte) => Some(*byte),
Err(nb::Error::WouldBlock) => None, Err(nb::Error::WouldBlock) => None,
Err(nb::Error::Other(e)) => { Err(nb::Error::Other(e)) => {
match e { match e {
Error::Overrun => { Error::Overrun => {
possible_error.overflow = true; errors.overflow = true;
} }
Error::FramingError => { Error::FramingError => {
possible_error.framing = true; errors.framing = true;
} }
Error::ParityError => { Error::ParityError => {
possible_error.parity = true; errors.parity = true;
} }
_ => { _ => {
possible_error.other = true; errors.other = true;
} }
} }
None 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 {
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;
} else {
break;
}
}
self.irq_completion_handler(&mut res);
return Ok(res);
} }
// RX transfer not complete, check for RX errors fn check_for_errors(&self, errors: &mut IrqUartError) {
if (self.irq_info.rx_idx < self.irq_info.rx_len) && rx_enabled {
// Read status register again, might have changed since reading received data // Read status register again, might have changed since reading received data
let rx_status = self.inner.0.rxstatus().read(); let rx_status = self.uart().rxstatus().read();
if rx_status.rxovr().bit_is_set() { if rx_status.rxovr().bit_is_set() {
res.errors.overflow = true; errors.overflow = true;
} }
if rx_status.rxfrm().bit_is_set() { if rx_status.rxfrm().bit_is_set() {
res.errors.framing = true; errors.framing = true;
} }
if rx_status.rxpar().bit_is_set() { if rx_status.rxpar().bit_is_set() {
res.errors.parity = true; errors.parity = true;
}
} }
// If it is not a timeout, it's an error fn irq_completion_handler_max_size_timeout(
if res.error() { &mut self,
res: &mut IrqResultMaxSizeTimeout,
context: &mut IrqContextTimeoutOrMaxSize,
) {
self.disable_rx_irq_sources(); self.disable_rx_irq_sources();
return Err(IrqError::Uart(res.errors)); self.0.disable();
} res.bytes_read = context.rx_idx;
}
// Clear the interrupt status bits
self.inner
.0
.irq_clr()
.write(|w| unsafe { w.bits(irq_end.bits()) });
Ok(res)
}
fn irq_completion_handler(&mut self, res: &mut IrqResult) {
self.disable_rx_irq_sources();
self.inner.disable();
res.bytes_read = self.irq_info.rx_idx;
res.complete = true; res.complete = true;
self.irq_info.mode = IrqReceptionMode::Idle; context.mode = IrqReceptionMode::Idle;
self.irq_info.rx_idx = 0; context.rx_idx = 0;
self.irq_info.rx_len = 0;
}
pub fn irq_info(&self) -> &IrqInfo {
&self.irq_info
} }
pub fn release(self) -> Uart { pub fn release(self) -> Uart {
self.inner.release() self.0.release()
} }
} }