Flashloader and UART improvements
Some checks are pending
Rust/va416xx-rs/pipeline/pr-main Build started...

This commit is contained in:
Robin Müller 2024-09-14 11:29:51 +02:00
parent 3e67749452
commit aae870c767
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_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;

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
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.

View File

@ -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,74 +279,57 @@ 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:
image_loader.handle_ping_cmd()
com_if.close()
return 0
if args.target:
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
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 = []
_LOGGER.info("Parsing ELF file for loadable sections")
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")):
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"
target == Target.BOOTLOADER
and segment.header.p_paddr != BOOTLOADER_START_ADDR
):
raise ValueError(
@ -177,7 +337,7 @@ def main() -> int:
f"bootloader, expected {BOOTLOADER_START_ADDR}"
)
if (
args.target == "a"
target == Target.APP_A
and segment.header.p_paddr != APP_A_START_ADDR
):
raise ValueError(
@ -185,7 +345,7 @@ def main() -> int:
f"App A, expected {APP_A_START_ADDR}"
)
if (
args.target == "b"
target == Target.APP_B
and segment.header.p_paddr != APP_B_START_ADDR
):
raise ValueError(
@ -214,101 +374,39 @@ def main() -> int:
)
)
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})"
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 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:
@ -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),
)

View File

@ -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<pac::Uart0>,
uart_tx: uart::Tx<pac::Uart0>,
rx_context: IrqContextTimeoutOrMaxSize,
rom_spi: Option<pac::Spi3>,
// We handle all TM in one task.
tm_cons: DataConsumer<BUF_RB_SIZE_TM, SIZES_RB_SIZE_TM>,
@ -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");
}
}
}

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
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

View File

@ -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<Hertz> 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<UartInstance, Pins> {
pins: Pins,
}
/// Serial receiver
/// Serial receiver.
///
/// Can be created by using the [Uart::split] or [UartBase::split] API.
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
///
/// Can be created by using the [Uart::split] or [UartBase::split] API.
pub struct Tx<Uart>(Uart);
impl<Uart: Instance> Rx<Uart> {
@ -551,33 +577,6 @@ impl<TxPinInst: TxPin<UartInstance>, RxPinInst: RxPin<UartInstance>, 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<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! {
to self.inner {
#[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> {
UartBase {
uart: self.inner.uart,
@ -651,6 +641,10 @@ impl<Uart: Instance> Rx<Uart> {
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 {
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<Uart>(Rx<Uart>);
impl<Uart: Instance> RxWithIrq<Uart> {
/// 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<Uart: Instance> RxWithIrq<Uart> {
#[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<Uart: Instance> RxWithIrq<Uart> {
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<IrqResult, IrqError> {
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<IrqResult, IrqUartError> {
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<Uart: Instance> RxWithIrq<Uart> {
// 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<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(
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<Uart: Instance> RxWithIrq<Uart> {
// 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<u8, Error>| -> Option<u8> {
// 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 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 {
Ok(byte) => Some(byte),
Ok(byte) => Some(*byte),
Err(nb::Error::WouldBlock) => None,
Err(nb::Error::Other(e)) => {
match e {
Error::Overrun => {
possible_error.overflow = true;
errors.overflow = true;
}
Error::FramingError => {
possible_error.framing = true;
errors.framing = true;
}
Error::ParityError => {
possible_error.parity = true;
errors.parity = true;
}
_ => {
possible_error.other = true;
errors.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 {
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
if (self.irq_info.rx_idx < self.irq_info.rx_len) && rx_enabled {
fn check_for_errors(&self, errors: &mut IrqUartError) {
// 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() {
res.errors.overflow = true;
errors.overflow = true;
}
if rx_status.rxfrm().bit_is_set() {
res.errors.framing = true;
errors.framing = true;
}
if rx_status.rxpar().bit_is_set() {
res.errors.parity = true;
errors.parity = true;
}
}
// If it is not a timeout, it's an error
if res.error() {
fn irq_completion_handler_max_size_timeout(
&mut self,
res: &mut IrqResultMaxSizeTimeout,
context: &mut IrqContextTimeoutOrMaxSize,
) {
self.disable_rx_irq_sources();
return Err(IrqError::Uart(res.errors));
}
}
// 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;
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()
}
}