diff --git a/.gitignore b/.gitignore index 1230da81..7c2e29f3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -/target/ +target/ .gdb_history [._]*.sw[a-p] **/*.rs.bk diff --git a/examples/nvmc-demo/Cargo.toml b/examples/nvmc-demo/Cargo.toml new file mode 100644 index 00000000..56bcf40d --- /dev/null +++ b/examples/nvmc-demo/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "nvmc-demo" +version = "0.1.0" +authors = ["Christopher Hunt"] +edition = "2018" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +cortex-m = "0.6.2" +cortex-m-rt = "0.6.12" +embedded-storage = "0.1.0" +rtt-target = {version = "0.2.0", features = ["cortex-m"] } + +[dependencies.embedded-hal] +version = "0.2.3" +features = ["unproven"] + +[dependencies.nrf52840-hal] +features = ["rt"] +path = "../../nrf52840-hal" +optional = true + +[features] +52840 = ["nrf52840-hal"] diff --git a/examples/nvmc-demo/Embed.toml b/examples/nvmc-demo/Embed.toml new file mode 100755 index 00000000..ca6fe29f --- /dev/null +++ b/examples/nvmc-demo/Embed.toml @@ -0,0 +1,63 @@ +[default.probe] +# USB vendor ID +# usb_vid = "1337" +# USB product ID +# usb_pid = "1337" +# Serial number +# serial = "12345678" +# The protocol to be used for communicating with the target. +protocol = "Swd" +# The speed in kHz of the data link to the target. +# speed = 1337 + +[default.flashing] +# Whether or not the target should be flashed. +enabled = true +# Whether or not the target should be halted after reset. +# DEPRECATED, moved to reset section +halt_afterwards = false +# Whether or not bytes erased but not rewritten with data from the ELF +# should be restored with their contents before erasing. +restore_unwritten_bytes = false +# The path where an SVG of the assembled flash layout should be written to. +# flash_layout_output_path = "out.svg" + +[default.reset] +# Whether or not the target should be reset. +# When flashing is enabled as well, the target will be reset after flashing. +enabled = true +# Whether or not the target should be halted after reset. +halt_afterwards = false + +[default.general] +# The chip name of the chip to be debugged. +chip = "nRF52840_xxAA" +# A list of chip descriptions to be loaded during runtime. +chip_descriptions = [] +# The default log level to be used. Possible values are one of: +# "OFF", "ERROR", "WARN", "INFO", "DEBUG", "TRACE" +log_level = "WARN" + +[default.rtt] +# Whether or not an RTTUI should be opened after flashing. +# This is exclusive and cannot be used with GDB at the moment. +enabled = true +# A list of channel associations to be displayed. If left empty, all channels are displayed. +channels = [ + # { up = 0, down = 0, name = "name", format = "String" } +] +# The duration in ms for which the logger should retry to attach to RTT. +timeout = 3000 +# Whether timestamps in the RTTUI are enabled +show_timestamps = true +# Whether to save rtt history buffer on exit. +log_enabled = false +# Where to save rtt history buffer relative to manifest path. +log_path = "./logs" + +[default.gdb] +# Whether or not a GDB server should be opened after flashing. +# This is exclusive and cannot be used with RTT at the moment. +enabled = false +# The connection string in host:port format wher the GDB server will open a socket. +# gdb_connection_string diff --git a/examples/nvmc-demo/build.rs b/examples/nvmc-demo/build.rs new file mode 100644 index 00000000..d534cc3d --- /dev/null +++ b/examples/nvmc-demo/build.rs @@ -0,0 +1,31 @@ +//! This build script copies the `memory.x` file from the crate root into +//! a directory where the linker can always find it at build time. +//! For many projects this is optional, as the linker always searches the +//! project root directory -- wherever `Cargo.toml` is. However, if you +//! are using a workspace or have a more complicated build setup, this +//! build script becomes required. Additionally, by requesting that +//! Cargo re-run the build script whenever `memory.x` is changed, +//! updating `memory.x` ensures a rebuild of the application with the +//! new memory settings. + +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; + +fn main() { + // Put `memory.x` in our output directory and ensure it's + // on the linker search path. + let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); + File::create(out.join("memory.x")) + .unwrap() + .write_all(include_bytes!("memory.x")) + .unwrap(); + println!("cargo:rustc-link-search={}", out.display()); + + // By default, Cargo will re-run a build script whenever + // any file in the project changes. By specifying `memory.x` + // here, we ensure the build script is only re-run when + // `memory.x` is changed. + println!("cargo:rerun-if-changed=memory.x"); +} diff --git a/examples/nvmc-demo/memory.x b/examples/nvmc-demo/memory.x new file mode 100644 index 00000000..ba852b53 --- /dev/null +++ b/examples/nvmc-demo/memory.x @@ -0,0 +1,35 @@ +MEMORY +{ + /* NOTE 1 K = 1 KiBi = 1024 bytes */ + FLASH : ORIGIN = 0x00000000, LENGTH = 1020K + CONFIG : ORIGIN = ORIGIN(FLASH) + LENGTH(FLASH), LENGTH = 4K /* 4K is the flash page size */ + RAM : ORIGIN = 0x20000000, LENGTH = 256K +} + +_config = ORIGIN(CONFIG); + +/* This is where the call stack will be allocated. */ +/* The stack is of the full descending type. */ +/* You may want to use this variable to locate the call stack and static + variables in different memory regions. Below is shown the default value */ +/* _stack_start = ORIGIN(RAM) + LENGTH(RAM); */ + +/* You can use this symbol to customize the location of the .text section */ +/* If omitted the .text section will be placed right after the .vector_table + section */ +/* This is required only on microcontrollers that store some configuration right + after the vector table */ +/* _stext = ORIGIN(FLASH) + 0x400; */ + +/* Example of putting non-initialized variables into custom RAM locations. */ +/* This assumes you have defined a region RAM2 above, and in the Rust + sources added the attribute `#[link_section = ".ram2bss"]` to the data + you want to place there. */ +/* Note that the section will not be zero-initialized by the runtime! */ +/* SECTIONS { + .ram2bss (NOLOAD) : ALIGN(4) { + *(.ram2bss); + . = ALIGN(4); + } > RAM2 + } INSERT AFTER .bss; +*/ diff --git a/examples/nvmc-demo/src/main.rs b/examples/nvmc-demo/src/main.rs new file mode 100644 index 00000000..a5101a89 --- /dev/null +++ b/examples/nvmc-demo/src/main.rs @@ -0,0 +1,50 @@ +#![no_std] +#![no_main] + +// Simple NVMC example + +#[cfg(feature = "52840")] +use nrf52840_hal as hal; + +use embedded_storage::nor_flash::NorFlash; +use embedded_storage::nor_flash::ReadNorFlash; +use hal::nvmc::Nvmc; +use rtt_target::{rprintln, rtt_init_print}; + +const CONFIG_SIZE: usize = 1024; +extern "C" { + #[link_name = "_config"] + static mut CONFIG: [u32; CONFIG_SIZE]; +} + +// To run this: `cargo embed --features "52840" --target thumbv7em-none-eabihf` + +#[cortex_m_rt::entry] +fn main() -> ! { + rtt_init_print!(); + + let p = hal::pac::Peripherals::take().unwrap(); + + #[cfg(feature = "52840")] + let mut nvmc = Nvmc::new(p.NVMC, unsafe { &mut CONFIG }); + + assert!(nvmc.try_erase(0, CONFIG_SIZE as u32 * 4).is_ok()); + let write_buf: [u8; 4] = [1, 2, 3, 4]; + assert!(nvmc.try_write(0, &write_buf).is_ok()); + let mut read_buf = [0u8; 2]; + assert!(nvmc.try_read(0, &mut read_buf).is_ok()); + assert_eq!(read_buf, write_buf[0..2]); + + rprintln!("What was written to flash was read!"); + + loop { + cortex_m::asm::wfe(); + } +} + +#[panic_handler] // panicking behavior +fn panic(_: &core::panic::PanicInfo) -> ! { + loop { + cortex_m::asm::bkpt(); + } +} diff --git a/nrf-hal-common/Cargo.toml b/nrf-hal-common/Cargo.toml index 33de3844..55dae69c 100644 --- a/nrf-hal-common/Cargo.toml +++ b/nrf-hal-common/Cargo.toml @@ -26,6 +26,7 @@ fixed = "1.0.0" rand_core = "0.6.3" cfg-if = "1.0.0" embedded-dma = "0.1.1" +embedded-storage = "0.1.0" [dependencies.void] default-features = false diff --git a/nrf-hal-common/src/lib.rs b/nrf-hal-common/src/lib.rs index 6d240329..c60da8eb 100644 --- a/nrf-hal-common/src/lib.rs +++ b/nrf-hal-common/src/lib.rs @@ -47,6 +47,8 @@ pub mod i2s; pub mod ieee802154; #[cfg(not(any(feature = "52811", feature = "52810", feature = "9160")))] pub mod lpcomp; +#[cfg(not(feature = "51"))] +pub mod nvmc; #[cfg(not(feature = "9160"))] pub mod ppi; #[cfg(not(feature = "51"))] diff --git a/nrf-hal-common/src/nvmc.rs b/nrf-hal-common/src/nvmc.rs new file mode 100644 index 00000000..10d55814 --- /dev/null +++ b/nrf-hal-common/src/nvmc.rs @@ -0,0 +1,202 @@ +//! HAL interface to the Non-Volatile Memory Controller (NVMC) peripheral. + +use core::ops::Deref; + +#[cfg(not(feature = "9160"))] +use crate::pac::nvmc; +#[cfg(feature = "9160")] +use crate::pac::nvmc_ns as nvmc; +#[cfg(not(feature = "9160"))] +use crate::pac::NVMC; +#[cfg(feature = "9160")] +use crate::pac::NVMC_NS as NVMC; + +use embedded_storage::nor_flash::{NorFlash, ReadNorFlash}; + +/// Interface to an NVMC instance. +pub struct Nvmc { + nvmc: T, + storage: &'static mut [u32], +} + +impl Nvmc +where + T: Instance, +{ + /// Takes ownership of the peripheral and storage area. + pub fn new(nvmc: T, storage: &'static mut [u32]) -> Nvmc { + Self { nvmc, storage } + } + + /// Consumes `self` and returns back the raw peripheral and associated storage. + pub fn free(self) -> (T, &'static mut [u32]) { + (self.nvmc, self.storage) + } + + fn enable_erase(&self) { + #[cfg(not(feature = "9160"))] + self.nvmc.config.write(|w| w.wen().een()); + #[cfg(feature = "9160")] + self.nvmc.configns.write(|w| w.wen().een()); + } + + fn enable_read(&self) { + #[cfg(not(feature = "9160"))] + self.nvmc.config.write(|w| w.wen().ren()); + #[cfg(feature = "9160")] + self.nvmc.configns.write(|w| w.wen().ren()); + } + + fn enable_write(&self) { + #[cfg(not(feature = "9160"))] + self.nvmc.config.write(|w| w.wen().wen()); + #[cfg(feature = "9160")] + self.nvmc.configns.write(|w| w.wen().wen()); + } + + #[inline] + fn wait_ready(&self) { + while !self.nvmc.ready.read().ready().bit_is_set() {} + } + + #[cfg(feature = "9160")] + #[inline] + fn wait_write_ready(&self) { + while !self.nvmc.readynext.read().readynext().bit_is_set() {} + } + + #[cfg(not(feature = "9160"))] + #[inline] + fn erase_page(&mut self, offset: usize) { + let bits = &mut (self.storage[offset as usize >> 2]) as *mut _ as u32; + self.nvmc.erasepage().write(|w| unsafe { w.bits(bits) }); + self.wait_ready(); + } + + #[cfg(feature = "9160")] + #[inline] + fn erase_page(&mut self, offset: usize) { + self.storage[offset as usize >> 2] = 0xffffffff; + self.wait_ready(); + } + + #[inline] + fn write_word(&mut self, offset: usize, word: u32) { + #[cfg(not(feature = "9160"))] + self.wait_ready(); + #[cfg(feature = "9160")] + self.wait_write_ready(); + self.storage[offset] = word; + cortex_m::asm::dmb(); + } +} + +impl ReadNorFlash for Nvmc +where + T: Instance, +{ + type Error = NvmcError; + + const READ_SIZE: usize = 4; + + fn try_read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> { + let offset = offset as usize; + let bytes_len = bytes.len(); + let read_len = bytes_len + (Self::READ_SIZE - (bytes_len % Self::READ_SIZE)); + let target_offset = offset + read_len; + if offset % Self::READ_SIZE == 0 && target_offset <= self.capacity() { + self.wait_ready(); + let last_offset = target_offset - Self::READ_SIZE; + for offset in (offset..last_offset).step_by(Self::READ_SIZE) { + let word = self.storage[offset >> 2]; + bytes[offset] = (word >> 24) as u8; + bytes[offset + 1] = (word >> 16) as u8; + bytes[offset + 2] = (word >> 8) as u8; + bytes[offset + 3] = (word >> 0) as u8; + } + let offset = last_offset; + let word = self.storage[offset >> 2]; + let mut bytes_offset = offset; + if bytes_offset < bytes_len { + bytes[bytes_offset] = (word >> 24) as u8; + bytes_offset += 1; + if bytes_offset < bytes_len { + bytes[bytes_offset] = (word >> 16) as u8; + bytes_offset += 1; + if bytes_offset < bytes_len { + bytes[bytes_offset] = (word >> 8) as u8; + bytes_offset += 1; + if bytes_offset < bytes_len { + bytes[bytes_offset] = (word >> 0) as u8; + } + } + } + } + Ok(()) + } else { + Err(NvmcError::Unaligned) + } + } + + fn capacity(&self) -> usize { + self.storage.len() << 2 + } +} + +impl NorFlash for Nvmc +where + T: Instance, +{ + const WRITE_SIZE: usize = 4; + + const ERASE_SIZE: usize = 4 * 1024; + + fn try_erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error> { + if from as usize % Self::ERASE_SIZE == 0 && to as usize % Self::ERASE_SIZE == 0 { + self.enable_erase(); + for offset in (from..to).step_by(Self::ERASE_SIZE) { + self.erase_page(offset as usize >> 2); + } + self.enable_read(); + Ok(()) + } else { + Err(NvmcError::Unaligned) + } + } + + fn try_write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> { + let offset = offset as usize; + if offset % Self::WRITE_SIZE == 0 && bytes.len() % Self::WRITE_SIZE == 0 { + self.enable_write(); + for offset in (offset..(offset + bytes.len())).step_by(Self::WRITE_SIZE) { + let word = ((bytes[offset] as u32) << 24) + | ((bytes[offset + 1] as u32) << 16) + | ((bytes[offset + 2] as u32) << 8) + | ((bytes[offset + 3] as u32) << 0); + self.write_word(offset >> 2, word); + } + self.enable_read(); + Ok(()) + } else { + Err(NvmcError::Unaligned) + } + } +} + +pub trait Instance: Deref + sealed::Sealed {} + +impl Instance for NVMC {} + +mod sealed { + use super::*; + + pub trait Sealed {} + + impl Sealed for NVMC {} +} + +#[derive(Debug)] +pub enum NvmcError { + /// An operation was attempted on an unaligned boundary + Unaligned, +} diff --git a/nrf52840-hal-tests/Cargo.toml b/nrf52840-hal-tests/Cargo.toml index 32eaf464..8e2a2d0d 100644 --- a/nrf52840-hal-tests/Cargo.toml +++ b/nrf52840-hal-tests/Cargo.toml @@ -20,6 +20,10 @@ harness = false name = "gpio-output-open-drain" harness = false +[[test]] +name = "nvmc" +harness = false + [[test]] name = "serial" harness = false @@ -29,6 +33,7 @@ cortex-m = "0.7.0" defmt = "0.2.0" defmt-rtt = "0.2.0" defmt-test = "0.2.0" +embedded-storage = "0.1.0" nrf52840-hal = { path = "../nrf52840-hal" } panic-probe = { version = "0.2.0", features = ["print-defmt"] } diff --git a/nrf52840-hal-tests/README.md b/nrf52840-hal-tests/README.md index ee7af492..c3d9ab17 100644 --- a/nrf52840-hal-tests/README.md +++ b/nrf52840-hal-tests/README.md @@ -1,6 +1,10 @@ # nRF52840 HAL tests -Run `cargo test -p nrf52840-hal-tests` to test the HAL on a nRF52840. +Run tests from the `cd nrf52840-hal-tests` folder as they require their own build considerations. + +Run `cargo test` to test the HAL on a nRF52840. + +To run a specific test: `cargo test --test nvmc`. The crate assumes that you'll test the HAL on a nRF52840 Development Kit. If you wish to use a different development board you'll need to update the flags passed to `probe-run` in `.cargo/config.toml`. diff --git a/nrf52840-hal-tests/build.rs b/nrf52840-hal-tests/build.rs new file mode 100644 index 00000000..d534cc3d --- /dev/null +++ b/nrf52840-hal-tests/build.rs @@ -0,0 +1,31 @@ +//! This build script copies the `memory.x` file from the crate root into +//! a directory where the linker can always find it at build time. +//! For many projects this is optional, as the linker always searches the +//! project root directory -- wherever `Cargo.toml` is. However, if you +//! are using a workspace or have a more complicated build setup, this +//! build script becomes required. Additionally, by requesting that +//! Cargo re-run the build script whenever `memory.x` is changed, +//! updating `memory.x` ensures a rebuild of the application with the +//! new memory settings. + +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::PathBuf; + +fn main() { + // Put `memory.x` in our output directory and ensure it's + // on the linker search path. + let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); + File::create(out.join("memory.x")) + .unwrap() + .write_all(include_bytes!("memory.x")) + .unwrap(); + println!("cargo:rustc-link-search={}", out.display()); + + // By default, Cargo will re-run a build script whenever + // any file in the project changes. By specifying `memory.x` + // here, we ensure the build script is only re-run when + // `memory.x` is changed. + println!("cargo:rerun-if-changed=memory.x"); +} diff --git a/nrf52840-hal-tests/memory.x b/nrf52840-hal-tests/memory.x new file mode 100644 index 00000000..ba852b53 --- /dev/null +++ b/nrf52840-hal-tests/memory.x @@ -0,0 +1,35 @@ +MEMORY +{ + /* NOTE 1 K = 1 KiBi = 1024 bytes */ + FLASH : ORIGIN = 0x00000000, LENGTH = 1020K + CONFIG : ORIGIN = ORIGIN(FLASH) + LENGTH(FLASH), LENGTH = 4K /* 4K is the flash page size */ + RAM : ORIGIN = 0x20000000, LENGTH = 256K +} + +_config = ORIGIN(CONFIG); + +/* This is where the call stack will be allocated. */ +/* The stack is of the full descending type. */ +/* You may want to use this variable to locate the call stack and static + variables in different memory regions. Below is shown the default value */ +/* _stack_start = ORIGIN(RAM) + LENGTH(RAM); */ + +/* You can use this symbol to customize the location of the .text section */ +/* If omitted the .text section will be placed right after the .vector_table + section */ +/* This is required only on microcontrollers that store some configuration right + after the vector table */ +/* _stext = ORIGIN(FLASH) + 0x400; */ + +/* Example of putting non-initialized variables into custom RAM locations. */ +/* This assumes you have defined a region RAM2 above, and in the Rust + sources added the attribute `#[link_section = ".ram2bss"]` to the data + you want to place there. */ +/* Note that the section will not be zero-initialized by the runtime! */ +/* SECTIONS { + .ram2bss (NOLOAD) : ALIGN(4) { + *(.ram2bss); + . = ALIGN(4); + } > RAM2 + } INSERT AFTER .bss; +*/ diff --git a/nrf52840-hal-tests/tests/nvmc.rs b/nrf52840-hal-tests/tests/nvmc.rs new file mode 100644 index 00000000..3bf7621b --- /dev/null +++ b/nrf52840-hal-tests/tests/nvmc.rs @@ -0,0 +1,100 @@ +#![no_std] +#![no_main] + +use defmt_rtt as _; +use nrf52840_hal as _; +use panic_probe as _; + +use embedded_storage::nor_flash::{NorFlash, ReadNorFlash}; +use nrf52840_hal::{nvmc::Nvmc, pac}; + +const CONFIG_SIZE: usize = 1024; +extern "C" { + #[link_name = "_config"] + static mut CONFIG: [u32; CONFIG_SIZE]; +} + +struct State { + nvmc: Nvmc, +} + +#[defmt_test::tests] +mod tests { + use defmt::{assert, unwrap}; + + use super::*; + + #[init] + fn init() -> State { + let p = unwrap!(pac::Peripherals::take()); + + State { + nvmc: Nvmc::new(p.NVMC, unsafe { &mut CONFIG }), + } + } + + #[test] + fn check_capacity(state: &mut State) { + assert_eq!(state.nvmc.capacity(), CONFIG_SIZE * 4); + } + + #[test] + fn read_unaligned(state: &mut State) { + let mut buf = [0u8; 1]; + assert!(state.nvmc.try_read(1, &mut buf).is_err()); + } + + #[test] + fn read_beyond_buffer(state: &mut State) { + let mut buf = [0u8; CONFIG_SIZE * 4 + 1]; + assert!(state.nvmc.try_read(0, &mut buf).is_err()); + } + + #[test] + fn erase_unaligned_from(state: &mut State) { + assert!(state.nvmc.try_erase(1, 4096).is_err()); + } + + #[test] + fn erase_unaligned_to(state: &mut State) { + assert!(state.nvmc.try_erase(0, 4097).is_err()); + } + + #[test] + fn write_unaligned(state: &mut State) { + let buf = [0u8; 1]; + assert!(state.nvmc.try_write(1, &buf).is_err()); + } + + #[test] + fn read_write_and_then_read(state: &mut State) { + assert!(state.nvmc.try_erase(0, CONFIG_SIZE as u32 * 4).is_ok()); + let mut read_buf = [0u8; 1]; + assert!(state.nvmc.try_read(0, &mut read_buf).is_ok()); + assert_eq!(read_buf[0], 0xff); + let write_buf = [1u8; 4]; + assert!(state.nvmc.try_write(0, &write_buf).is_ok()); + assert!(state.nvmc.try_read(0, &mut read_buf).is_ok()); + assert_eq!(read_buf[0], 0x1); + } + + #[test] + fn read_what_is_written(state: &mut State) { + assert!(state.nvmc.try_erase(0, CONFIG_SIZE as u32 * 4).is_ok()); + let write_buf: [u8; 8] = [1, 2, 3, 4, 5, 6, 7, 8]; + assert!(state.nvmc.try_write(0, &write_buf).is_ok()); + let mut read_buf = [0u8; 8]; + assert!(state.nvmc.try_read(0, &mut read_buf).is_ok()); + assert_eq!(read_buf, write_buf); + } + + #[test] + fn partially_read_what_is_written(state: &mut State) { + assert!(state.nvmc.try_erase(0, CONFIG_SIZE as u32 * 4).is_ok()); + let write_buf: [u8; 4] = [1, 2, 3, 4]; + assert!(state.nvmc.try_write(0, &write_buf).is_ok()); + let mut read_buf = [0u8; 2]; + assert!(state.nvmc.try_read(0, &mut read_buf).is_ok()); + assert_eq!(read_buf, write_buf[0..2]); + } +} diff --git a/xtask/src/lib.rs b/xtask/src/lib.rs index b9276d8f..d570ac45 100644 --- a/xtask/src/lib.rs +++ b/xtask/src/lib.rs @@ -27,6 +27,7 @@ pub static EXAMPLES: &[(&str, &[&str])] = &[ "ppi-demo", &["51", "52810", "52811", "52832", "52833", "52840"], ), + ("nvmc-demo", &["52840"]), ("pwm-blinky-demo", &["52840"]), ("pwm-demo", &["52840"]), ("qdec-demo", &[]),