diff --git a/.cargo/config b/.cargo/config new file mode 100644 index 0000000..7a738d6 --- /dev/null +++ b/.cargo/config @@ -0,0 +1,8 @@ +[build] +target = 'thumbv7em-none-eabi' + +[target.'cfg(all(target_arch = "arm", target_os = "none"))'] +runner = 'arm-none-eabi-gdb' +rustflags = [ + "-C", "link-arg=-Tlink.x", +] diff --git a/.gdbinit b/.gdbinit new file mode 100644 index 0000000..68963eb --- /dev/null +++ b/.gdbinit @@ -0,0 +1,35 @@ +# disable "are you sure you want to quit?" +define hook-quit + set confirm off +end + +target extended-remote :3333 + +# print demangled symbols +set print asm-demangle on + +# set backtrace limit to not have infinite backtrace loops +set backtrace limit 32 + +# detect unhandled exceptions, hard faults and panics +break DefaultHandler +break HardFault +break rust_begin_unwind + +monitor arm semihosting enable + +# # send captured ITM to the file itm.fifo +# # (the microcontroller SWO pin must be connected to the programmer SWO pin) +# # 8000000 must match the core clock frequency +# monitor tpiu config internal itm.txt uart off 8000000 + +# # OR: make the microcontroller SWO pin output compatible with UART (8N1) +# # 8000000 must match the core clock frequency +# # 2000000 is the frequency of the SWO pin +# monitor tpiu config external uart off 8000000 2000000 + +# # enable ITM port 0 +# monitor itm port 0 on + +load +cont diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a9d37c5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..daf1454 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,21 @@ +language: rust +rust: + - 1.33.0 + - stable + - nightly +cache: cargo +sudo: false +env: + global: + - RUSTFLAGS="--deny warnings" + - RUST_BACKTRACE=1 + - CARGO_INCREMENTAL=0 # decrease size of `target` to make the cache smaller + matrix: + - FEATURES="" # default configuration + - FEATURES="--all-features" +script: + - cargo build --all --examples $FEATURES + - cargo build --all --examples --release $FEATURES +notifications: + email: + on_success: never diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..63bfc98 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## Unreleased + +Initial release. diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4b952dc --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "spi-memory" +version = "0.0.0" +authors = ["Jonas Schievink "] +edition = "2018" +description = "A generic driver for different SPI Flash and EEPROM chips" +documentation = "https://docs.rs/spi-memory/" +repository = "https://github.com/jonas-schievink/spi-memory.git" +keywords = ["embedded-hal-driver", "serial", "flash", "eeprom", "spi"] +categories = ["embedded"] +readme = "README.md" +license = "0BSD" + +[dependencies] +embedded-hal = "0.2.3" +log = { version = "0.4.6", optional = true } +bitflags = "1.0.4" + +[dev-dependencies] +cortex-m = "0.6.0" +cortex-m-rt = "0.6.8" +cortex-m-semihosting = "0.3.3" +stm32f4xx-hal = { version = "0.5.0", features = ["stm32f401"] } +panic-semihosting = "0.5.2" + +[profile.dev] +opt-level = "z" +panic = "abort" + +# cargo-release configuration +[package.metadata.release] +tag-message = "{{version}}" +no-dev-version = true +pre-release-commit-message = "Release {{version}}" + +# Change the changelog's `Unreleased` section to refer to this release and +# prepend new `Unreleased` section +[[package.metadata.release.pre-release-replacements]] +file = "CHANGELOG.md" +search = "## Unreleased" +replace = "## Unreleased\n\nNo changes.\n\n## {{version}} - {{date}}" + +# Bump the version inside the example manifest in `README.md` +[[package.metadata.release.pre-release-replacements]] +file = "README.md" +search = 'spi-memory = "[a-z0-9\\.-]+"' +replace = 'spi-memory = "{{version}}"' + +# Bump the version referenced by the `html_root_url` attribute in `lib.rs` +[[package.metadata.release.pre-release-replacements]] +file = "src/lib.rs" +search = "https://docs.rs/spi-memory/[a-z0-9\\.-]+" +replace = "https://docs.rs/spi-memory/{{version}}" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..89336aa --- /dev/null +++ b/LICENSE @@ -0,0 +1,12 @@ +Copyright (C) Jonas Schievink + +Permission to use, copy, modify, and/or distribute this software for +any purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN +AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d85ae97 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# `spi-memory` + +[![crates.io](https://img.shields.io/crates/v/spi-memory.svg)](https://crates.io/crates/spi-memory) +[![docs.rs](https://docs.rs/spi-memory/badge.svg)](https://docs.rs/spi-memory/) +[![Build Status](https://travis-ci.org/jonas-schievink/spi-memory.svg?branch=master)](https://travis-ci.org/jonas-schievink/spi-memory) + +This crate provides a generic [`embedded-hal`]-based driver for different +families of SPI Flash and EEPROM chips. + +Right now, only 25-series Flash chips are supported. Feel free to send PRs to +support other families though! + +Please refer to the [changelog](CHANGELOG.md) to see what changed in the last +releases. + +[`embedded-hal`]: https://github.com/rust-embedded/embedded-hal + +## Usage + +Add an entry to your `Cargo.toml`: + +```toml +[dependencies] +spi-memory = "0.0.0" +``` + +Check the [API Documentation](https://docs.rs/spi-memory/) for how to use the +crate's functionality. diff --git a/RELEASE_PROCESS.md b/RELEASE_PROCESS.md new file mode 100644 index 0000000..5d0bfd7 --- /dev/null +++ b/RELEASE_PROCESS.md @@ -0,0 +1,13 @@ +# What to do to publish a new release + +1. Ensure all notable changes are in the changelog under "Unreleased". + +2. Execute `cargo release ` to bump version(s), tag and publish + everything. External subcommand, must be installed with `cargo install + cargo-release`. + + `` can be one of `major|minor|patch`. If this is the first release + (`0.1.0`), use `minor`, since the version start out as `0.0.0`. + +3. Go to the GitHub releases, edit the just-pushed tag. Copy the release notes + from the changelog. diff --git a/examples/dump.rs b/examples/dump.rs new file mode 100644 index 0000000..a16401c --- /dev/null +++ b/examples/dump.rs @@ -0,0 +1,98 @@ +//! A Nucleo-64 F401 example that dumps flash contents to a USART. +//! +//! The flash chip is connected to the canonical SPI port on the Arduino-style +//! connector: +//! +//! * SCK = D13 = PA5 +//! * MISO = D12 = PA6 +//! * MOSI = D11 = PA7 +//! +//! The data is dumped in hexadecimal format through USART2 (TX = D1 = PA2). + +#![no_std] +#![no_main] + +extern crate panic_semihosting; + +use cortex_m_rt::entry; +use stm32f4xx_hal::spi::Spi; +use stm32f4xx_hal::stm32 as pac; +use stm32f4xx_hal::gpio::GpioExt; +use stm32f4xx_hal::time::{Bps, MegaHertz}; +use stm32f4xx_hal::rcc::RccExt; +use stm32f4xx_hal::serial::{self, Serial}; +use embedded_hal::spi::MODE_0; +use embedded_hal::digital::v2::OutputPin; +use embedded_hal::serial::Write; +use cortex_m_semihosting::hprintln; + +use spi_memory::series25::Flash; + +use core::fmt::Write as _; + +/// Flash chip size in Mbit. +const MEGABITS: u32 = 4; + +/// Serial baudrate. +const BAUDRATE: u32 = 912600; + +/// Size of the flash chip in bytes. +const SIZE_IN_BYTES: u32 = (MEGABITS * 1024 * 1024) / 8; + + +fn print<'a, E>(buf: &[u8], w: &'a mut (dyn Write + 'static)) { + for c in buf { + write!(w, "{:02X}", c).unwrap(); + } + writeln!(w).unwrap(); +} + +#[entry] +fn main() -> ! { + let periph = pac::Peripherals::take().unwrap(); + let clocks = periph.RCC.constrain().cfgr.freeze(); + let gpioa = periph.GPIOA.split(); + + let cs = { + let mut cs = gpioa.pa9.into_push_pull_output(); + cs.set_high().unwrap(); // deselect + cs + }; + + let spi = { + let sck = gpioa.pa5.into_alternate_af5(); + let miso = gpioa.pa6.into_alternate_af5(); + let mosi = gpioa.pa7.into_alternate_af5(); + + Spi::spi1(periph.SPI1, (sck, miso, mosi), MODE_0, MegaHertz(1).into(), clocks) + }; + + let mut serial = { + let tx = gpioa.pa2.into_alternate_af7(); + + let config = serial::config::Config { + baudrate: Bps(BAUDRATE), + ..Default::default() + }; + Serial::usart2(periph.USART2, (tx, serial::NoRx), config, clocks).unwrap() + }; + + let mut flash = Flash::init(spi, cs).unwrap(); + let id = flash.read_jedec_id().unwrap(); + hprintln!("{:?}", id).ok(); + + let mut addr = 0; + const BUF: usize = 32; + let mut buf = [0; BUF]; + + while addr < SIZE_IN_BYTES { + flash.read(addr, &mut buf).unwrap(); + print(&buf, &mut serial); + + addr += BUF as u32; + } + + hprintln!("DONE").ok(); + + loop {} +} diff --git a/memory.x b/memory.x new file mode 100644 index 0000000..6dba9cf --- /dev/null +++ b/memory.x @@ -0,0 +1,11 @@ +MEMORY +{ + /* NOTE K = KiBi = 1024 bytes */ + FLASH : ORIGIN = 0x08000000, LENGTH = 64K + RAM : ORIGIN = 0x20000000, LENGTH = 32K +} + +/* This is where the call stack will be allocated. */ +/* The stack is of the full descending type. */ +/* NOTE Do NOT modify `_stack_start` unless you know what you are doing */ +_stack_start = ORIGIN(RAM) + LENGTH(RAM); diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..fe7171b --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,61 @@ +//! An `embedded-hal`-based SPI-Flash chip driver. +//! +//! This crate aims to be compatible with common families of SPI flash chips. +//! Currently, reading 25-series chips is supported, and support for writing and +//! erasing as well as other chip families (eg. 24-series chips) is planned. +//! Contributions are always welcome! + +#![doc(html_root_url = "https://docs.rs/spi-memory/0.0.0")] +#![warn(missing_debug_implementations, rust_2018_idioms)] +#![no_std] + +#[macro_use] +mod log; +pub mod series25; +mod utils; + +use core::fmt::{self, Debug}; +use embedded_hal::blocking::spi::Transfer; +use embedded_hal::digital::v2::OutputPin; + +mod private { + #[derive(Debug)] + pub enum Private {} +} + +/// The error type used by this library. +/// +/// This can encapsulate an SPI or GPIO error, and adds its own protocol errors +/// on top of that. +pub enum Error, GPIO: OutputPin> { + /// An SPI transfer failed. + Spi(SPI::Error), + + /// A GPIO could not be set. + Gpio(GPIO::Error), + + /// Status register contained unexpected flags. + /// + /// This can happen when the chip is faulty, incorrectly connected, or the + /// driver wasn't constructed or destructed properly (eg. while there is + /// still a write in progress). + UnexpectedStatus, + + #[doc(hidden)] + __NonExhaustive(private::Private), +} + +impl, GPIO: OutputPin> Debug for Error +where + SPI::Error: Debug, + GPIO::Error: Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::Spi(spi) => write!(f, "Error::Spi({:?})", spi), + Error::Gpio(gpio) => write!(f, "Error::Gpio({:?})", gpio), + Error::UnexpectedStatus => f.write_str("Error::UnexpectedStatus"), + Error::__NonExhaustive(_) => unreachable!(), + } + } +} diff --git a/src/log.rs b/src/log.rs new file mode 100644 index 0000000..a84809a --- /dev/null +++ b/src/log.rs @@ -0,0 +1,51 @@ +#![allow(unused_macros)] + +#[cfg(feature = "log")] +macro_rules! error { + ($($t:tt)*) => {{ log::error!($($t)*); }}; +} + +#[cfg(feature = "log")] +macro_rules! warn { + ($($t:tt)*) => {{ log::warn!($($t)*); }}; +} + +#[cfg(feature = "log")] +macro_rules! info { + ($($t:tt)*) => {{ log::info!($($t)*); }}; +} + +#[cfg(feature = "log")] +macro_rules! debug { + ($($t:tt)*) => {{ log::debug!($($t)*); }}; +} + +#[cfg(feature = "log")] +macro_rules! trace { + ($($t:tt)*) => {{ log::trace!($($t)*); }}; +} + +#[cfg(not(feature = "log"))] +macro_rules! error { + ($($t:tt)*) => {{ format_args!($($t)*); }}; +} + +#[cfg(not(feature = "log"))] +macro_rules! warn { + ($($t:tt)*) => {{ format_args!($($t)*); }}; +} + +#[cfg(not(feature = "log"))] +macro_rules! info { + ($($t:tt)*) => {{ format_args!($($t)*); }}; +} + +#[cfg(not(feature = "log"))] +macro_rules! debug { + ($($t:tt)*) => {{ format_args!($($t)*); }}; +} + +#[cfg(not(feature = "log"))] +macro_rules! trace { + ($($t:tt)*) => {{ format_args!($($t)*); }}; +} diff --git a/src/series25.rs b/src/series25.rs new file mode 100644 index 0000000..6dcdbd6 --- /dev/null +++ b/src/series25.rs @@ -0,0 +1,160 @@ +//! Driver for 25-series SPI Flash and EEPROM chips. + +use crate::{utils::HexSlice, Error}; +use bitflags::bitflags; +use core::fmt; +use embedded_hal::blocking::spi::Transfer; +use embedded_hal::digital::v2::OutputPin; + +/// 3-Byte JEDEC manufacturer and device identification. +pub struct Identification { + /// The received bytes, in order. + /// + /// First 1 or 2 Bytes are the JEDEC manufacturer ID, last 1-2 Bytes are the + /// device ID. How many bytes are used depends on manufacturer's place in + /// the JEDEC list, I guess. + bytes: [u8; 3], +} + +impl fmt::Debug for Identification { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("Identification") + .field(&HexSlice(self.bytes)) + .finish() + } +} + +#[allow(unused)] // TODO support more features +enum Opcode { + /// Read the 8-bit legacy device ID. + ReadDeviceId = 0xAB, + /// Read the 8-bit manufacturer and device IDs. + ReadMfDId = 0x90, + /// Read the 16-bit manufacturer ID and 8-bit device ID. + ReadJedecId = 0x9F, + /// Set the write enable latch. + WriteEnable = 0x06, + /// Clear the write enable latch. + WriteDisable = 0x04, + /// Read the 8-bit status register. + ReadStatus = 0x05, + /// Write the 8-bit status register. Not all bits are writeable. + WriteStatus = 0x01, + Read = 0x03, + PageProg = 0x02, // directly writes to EEPROMs too + SectorErase = 0xD7, + BlockErase = 0xD8, + ChipErase = 0xC7, +} + +bitflags! { + /// Status register bits. + pub struct Status: u8 { + /// **W**rite **I**n **P**rogress bit. + const WIP = 1 << 0; + /// Status of the **W**rite **E**nable **L**atch. + const WEL = 1 << 1; + /// The 3 protection region bits. + const PROT = 0b00011100; + /// **S**tatus **R**egister **W**rite **D**isable bit. + const SRWD = 1 << 7; + } +} + +/// Driver for 25-series SPI Flash chips. +/// +/// # Type Parameters +/// +/// * **`SPI`**: The SPI master to which the flash chip is attached. +/// * **`CS`**: The **C**hip-**S**elect line attached to the `\CS`/`\CE` pin of +/// the flash chip. +#[derive(Debug)] +pub struct Flash, CS: OutputPin> { + spi: SPI, + cs: CS, +} + +impl, CS: OutputPin> Flash { + /// Creates a new 25-series flash driver. + /// + /// # Parameters + /// + /// * **`spi`**: An SPI master. Must be configured to operate in the correct + /// mode for the device. + /// * **`cs`**: The **C**hip-**S**elect Pin connected to the `\CS`/`\CE` pin + /// of the flash chip. Will be driven low when accessing the device. + pub fn init(spi: SPI, cs: CS) -> Result> { + let mut this = Self { spi, cs }; + let status = this.read_status()?; + info!("Flash::init: status = {:?}", status); + + // Here we don't expect any writes to be in progress, and the latch must + // also be deasserted. + if !(status & (Status::WIP | Status::WEL)).is_empty() { + return Err(Error::UnexpectedStatus); + } + + Ok(this) + } + + fn command(&mut self, bytes: &mut [u8]) -> Result<(), Error> { + // If the SPI transfer fails, make sure to disable CS anyways + self.cs.set_low().map_err(Error::Gpio)?; + let spi_result = self.spi.transfer(bytes).map_err(Error::Spi); + self.cs.set_high().map_err(Error::Gpio)?; + spi_result?; + Ok(()) + } + + /// Reads the JEDEC manufacturer/device identification. + pub fn read_jedec_id(&mut self) -> Result> { + let mut buf = [Opcode::ReadJedecId as u8, 0, 0, 0]; + self.command(&mut buf)?; + + Ok(Identification { + bytes: [buf[1], buf[2], buf[3]], + }) + } + + /// Reads the status register. + pub fn read_status(&mut self) -> Result> { + let mut buf = [Opcode::ReadStatus as u8, 0]; + self.command(&mut buf)?; + + Ok(Status::from_bits_truncate(buf[1])) + } + + /// Reads flash contents into `buf`, starting at `addr`. + /// + /// This will always read `buf.len()` worth of bytes, filling up `buf` + /// completely. + /// + /// Note that `addr` is not fully decoded: Flash chips will typically only + /// look at the lowest `N` bits needed to encode their size, which means + /// that the contents are "mirrored" to addresses that are a multiple of the + /// flash size. Only 24 bits of `addr` are transferred to the device in any + /// case, limiting the maximum size of 25-series SPI flash chips to 16 MiB. + /// + /// # Parameters + /// + /// * **`addr`**: 24-bit address to start reading at. + /// * **`buf`**: Destination buffer to fill. + pub fn read(&mut self, addr: u32, buf: &mut [u8]) -> Result<(), Error> { + // TODO what happens if `buf` is empty? + + let mut cmd_buf = [ + Opcode::Read as u8, + (addr >> 16) as u8, + (addr >> 8) as u8, + addr as u8, + ]; + + self.cs.set_low().map_err(Error::Gpio)?; + let mut spi_result = self.spi.transfer(&mut cmd_buf); + if spi_result.is_ok() { + spi_result = self.spi.transfer(buf); + } + self.cs.set_high().map_err(Error::Gpio)?; + spi_result.map(|_| ()).map_err(Error::Spi) + } +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..45abc88 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,18 @@ +use core::fmt; + +pub struct HexSlice(pub T) +where + T: AsRef<[u8]>; + +impl> fmt::Debug for HexSlice { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("[")?; + for (i, byte) in self.0.as_ref().iter().enumerate() { + if i != 0 { + f.write_str(", ")?; + } + write!(f, "{:02x}", byte)?; + } + f.write_str("]") + } +}