From 3559aed2016de6b555c0e0ba05ab7324679967f6 Mon Sep 17 00:00:00 2001 From: Jonas Schievink Date: Fri, 24 May 2019 23:43:22 +0200 Subject: [PATCH] Initial commit --- .cargo/config | 8 +++ .gdbinit | 35 ++++++++++ .gitignore | 2 + .travis.yml | 21 ++++++ CHANGELOG.md | 5 ++ Cargo.toml | 53 +++++++++++++++ LICENSE | 12 ++++ README.md | 28 ++++++++ RELEASE_PROCESS.md | 13 ++++ examples/dump.rs | 98 +++++++++++++++++++++++++++ memory.x | 11 ++++ src/lib.rs | 61 +++++++++++++++++ src/log.rs | 51 +++++++++++++++ src/series25.rs | 160 +++++++++++++++++++++++++++++++++++++++++++++ src/utils.rs | 18 +++++ 15 files changed, 576 insertions(+) create mode 100644 .cargo/config create mode 100644 .gdbinit create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 CHANGELOG.md create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 RELEASE_PROCESS.md create mode 100644 examples/dump.rs create mode 100644 memory.x create mode 100644 src/lib.rs create mode 100644 src/log.rs create mode 100644 src/series25.rs create mode 100644 src/utils.rs 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("]") + } +}