commit ec9805b01ae00390795de5355e52bcc6ec2d74f1 Author: conky Date: Mon Aug 31 15:45:58 2026 +0300 Initial commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e64f06a --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.pio +.omo +.junie +.idea diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..aa2ab39 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,104 @@ +# AGENTS.md + +PlatformIO / Arduino-framework firmware for a NUCLEO-F042K6 reading two MS5611 +barometers (one I2C, one SPI) and emitting validated CSV over the ST-LINK VCP. +See `README.md` for wiring tables, CSV format and status codes. + +## Commands + +```sh +pio run # build (the only real verification gate) +pio run -t upload # flash over ST-LINK +pio device monitor # 115200 baud +pio run -t clean +``` + +There is one env, `nucleo_f042k6`. No lint, format, or typecheck step exists. + +**Always read the size report at the end of `pio run`.** It is the acceptance +criterion for any change (see below). Current: flash 88.8% (29108 / 32768), +RAM 32.6% (2004 / 6144). + +## Flash budget is the dominant constraint + +3.6 KB of flash headroom. This shapes almost every design decision here, and it +is the single easiest thing to break without noticing. + +`-flto` and `-fsingle-precision-constant` in `platformio.ini` are load-bearing. +Verified: building without them overflows `FLASH` by **7252 bytes** and fails to +link. Both MS5611 driver libraries write their compensation maths with unsuffixed +double literals, which otherwise links the double soft-float helpers. + +Consequences for new code: + +- Never introduce `double`, unsuffixed floating literals, or `` calls on + doubles. Use `float` and `f`-suffixed constants. +- Never call `Serial.print(someFloat, digits)` — it resolves to + `Print::print(double, int)` and pulls in soft-float. `main.cpp::printFixed2()` + exists solely to format floats via scaled integers; use it. +- Wrap string literals in `F()` so they stay in flash. +- No `String`, no `new`/`delete`. `~BaroChannel()` is deliberately non-virtual + and `protected` to keep a destructor slot out of the vtable and avoid linking + `operator delete`. Do not "fix" this into a virtual destructor. +- `lib_deps` pin exact driver tags (`MS5611#0.5.2`, `MS5611_SPI#0.4.3`). Bumping + them can blow the budget; rebuild and check size if you do. + +## Architecture + +- `include/config.h` — every pin, address, rate, threshold and validation limit. + Change hardware behaviour **here**, not in the `.cpp` files. +- `include/baro_channel.h` / `src/baro_channel.cpp` — `BaroChannel`, the shared + state machine: init + PROM/CRC handshake, per-poll validation, error streaks, + re-init, and the 1 s error LED pulse. Five pure-virtual `driver*()` hooks are + the only per-bus surface. +- `src/baro_i2c.cpp` / `src/baro_spi.cpp` — one concrete subclass each, plus a + file-static instance exposed through `baroI2cChannel()` / `baroSpiChannel()`. +- `src/ms5611_crc.cpp` — standalone PROM CRC-4 (AN520). +- `src/main.cpp` — `setup()`/`loop()`, CSV formatting, 10 Hz report scheduling. + +**The two-file split is mandatory, not stylistic.** `MS5611.h` and +`MS5611_SPI.h` each define their own `enum osr_t` and `MS5611_READ_OK`, so they +cannot be included in the same translation unit. `baro_channel.h` therefore +includes neither driver header, and the channels are handed out via accessor +functions. Do not merge these TUs or hoist a driver include into the header. + +## Conventions that differ from defaults + +- **millis() rollover safety**: deadline comparisons use a signed difference — + `if ((int32_t)(now - deadline) >= 0)`. Never write `now >= deadline`. +- **`poll()` blocks** for ~2 ADC conversions (~5 ms/sensor at OSR 1024). + `main.cpp` re-reads `millis()` after polling before evaluating LED and report + deadlines. Preserve that if you touch the loop. +- **Validation belongs in `BaroChannel`, not the drivers.** Neither library + checks the PROM CRC, and `MS5611::read()` returns `MS5611_READ_OK` for an ADC + read taken before the conversion completed (which yields 0). Do not simplify + the range / stale / all-zero-PROM checks away by trusting driver return codes. +- Style is Allman braces, 2-space indent, `_camelCase` private members, `// ` + (two spaces) block comments. There is **no `.clang-format`** in the repo — + running `clang-format` would reformat everything into LLVM style. Don't. +- `SPI` and `I2C` appear in `lib_deps`, but the code uses `Wire` directly and + nothing includes the third-party `I2C` library. + +## Testing + +There is no automated test suite. `test/` holds only the stock PlatformIO +README, and `pio test` collects 0 cases. Do not claim tests pass. + +`ms5611_crc.{h,cpp}` is intentionally free of Arduino headers so it can be +exercised on the host: + +```sh +clang++ -std=c++17 -Wall -Wextra -c src/ms5611_crc.cpp -Iinclude -o /tmp/crc.o +``` + +The differential check against the NuttX reference described in `README.md` was +a host-side exercise and is not committed. + +Anything beyond the CRC needs real hardware: build, flash, and watch the CSV +stream. Unplugging a sensor is the quick way to exercise the error paths +(`ERR_INIT` / `ERR_CRC`, LED pulse, auto re-init after 10 consecutive failures). + +## Notes + +`.pio/`, `.omo/`, `.junie/` and `.idea/` are gitignored tooling state, not +project sources. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8295997 --- /dev/null +++ b/README.md @@ -0,0 +1,146 @@ +# Barometric pressure test + +## Goals + +1. Read barometric pressure from two sensors. One connected to the MCU with SPI and the other connected with I2C bus. +2. The reading must be done in real time and data should be checked for validity. +3. The output should be sent to a serial port. +4. In case of reading error the LED should be turned on for 1 second. Each sensor has its own LED. + +## Hardware +- MCU - STM32F042K6T6 (Nucleo-32, board MB1180) - 32 KB flash, 6 KB RAM +- Barometric pressure sensor - 2x GY-63 (MS5611) + +## Wiring + +Pin assignments follow ST UM1956 Table 10 and the MB1180 C.2 schematic, and match +the STM32duino defaults for this variant, so `Wire.begin()` and `SPI.begin()` need +no explicit pin overrides. + +![Wiring diagram](docs/wiring.png) + +Vector source: [`docs/wiring.svg`](docs/wiring.svg). Pins there are grouped by +function, not by physical header order. + +The same wiring drawn physically, with the real header order from UM1956 +Table 10, so it is clear which hole each jumper goes into: + +![Physical wiring diagram](docs/wiring-physical.png) + +Vector source: [`docs/wiring-physical.svg`](docs/wiring-physical.svg). Note that +SPI is split across both connectors: MOSI, MISO and CS are on CN3, while SCK is +CN4 pin 15. + +### Baro0 - I2C (GY-63 <=> STM32F042) +| GY-63 | Nucleo | STM32 | Note | +|-------|--------|-------|------| +| VCC | 3V3 | - | | +| GND | GND | - | | +| PS | 3.3V | - | high selects I2C | +| SDA | A4 | PB7 | `PIN_WIRE_SDA`, reaches the A4 pad through SB18 | +| SCL | A5 | PB6 | `PIN_WIRE_SCL`, reaches the A5 pad through SB16 | +| CSB | GND | - | sets address `0x77`; tie to VCC for `0x76` | + +The MS5611 address is `1110 11Cx`, where C is the **complement** of CSB +(datasheet p.12). `CSB` must not be left floating. If you strap it high, change +`BARO_I2C_ADDRESS` in [`include/config.h`](include/config.h). + +### Baro1 - SPI (GY-63 <=> STM32F042) +| GY-63 | Nucleo | STM32 | Note | +|-------|--------|-------|------| +| VCC | 3V3 | - | | +| GND | GND | - | | +| PS | GND | - | low selects SPI | +| SCLK | D13 | PB3 | SPI1_SCK | +| SDI | D11 | PB5 | SPI1_MOSI | +| SDO | D12 | PB4 | SPI1_MISO | +| CSB | D10 | PA11 | chip select, `BARO_SPI_CS_PIN` | + +SPI1 has to stay on this pin group. The alternative group (PA5/PA6/PA7) is +unusable while I2C is active: with the factory-default solder bridges SB16 and +SB18 closed, PA6 shares a net with PB6 (SCL) and PA5 shares a net with PB7 (SDA). + +### Error LEDs + +Two external LEDs, one per sensor, each in series with roughly 510 R to GND. + +| Signal | Nucleo | STM32 | +|--------|--------|-------| +| I2C sensor error | D3 | PB0 | +| SPI sensor error | D6 | PB1 | + +Both pins are set in [`include/config.h`](include/config.h). + +## Build and run + +```sh +pio run # build +pio run -t upload # flash over ST-LINK +pio device monitor # 115200 baud, ST-LINK Virtual COM Port +``` + +`Serial` is USART2 on PA2/PA15, wired to the ST-LINK VCP, so no USB-serial +adapter is needed. + +The image is close to the flash ceiling (about 89% of 32 KB). `-flto` and +`-fsingle-precision-constant` in [`platformio.ini`](platformio.ini) are required, +not cosmetic: both MS5611 drivers write their compensation maths with unsuffixed +double literals, which otherwise links roughly 5 KB of double soft-float helpers +and overflows flash. + +## Output + +CSV at 10 Hz on the serial port, with a header line printed once at startup: + +``` +ms,i2c_status,i2c_p_mbar,i2c_t_c,spi_status,spi_p_mbar,spi_t_c +1043,OK,1013.24,24.31,OK,1013.19,24.44 +1143,OK,1013.25,24.31,ERR_READ,1013.19,24.44 +``` + +Pressure is in mbar, temperature in degrees C, both to two decimals. The status +column is authoritative: when it is not `OK`, the two values next to it are the +last ones successfully computed, not fresh readings. + +| Status | Meaning | +|--------|---------| +| `INIT` | not initialised yet | +| `OK` | reading passed every check | +| `ERR_INIT` | reset / PROM handshake did not complete | +| `ERR_CRC` | factory calibration failed CRC-4, or is all-zero / all-ones | +| `ERR_READ` | driver reported a bus or ADC failure | +| `ERR_RANGE` | value outside the MS5611 operating envelope | +| `ERR_STALE` | sensor still answers but stopped producing new samples | + +Sensors are polled continuously, as fast as the ADC conversions allow (roughly +90 Hz per sensor at OSR 1024); the serial report is throttled to 10 Hz. + +## Validation + +Neither driver library detects every failure on its own, so the checks live in +[`BaroChannel`](src/baro_channel.cpp): + +- **PROM CRC-4** at init, per datasheet p.13 / AN520. Neither library verifies it. +- **All-zero / all-ones PROM screening**, because an absent part reads back as + one of those, `0xFFFF` is accepted by both libraries' `reset()`, and an + all-zero PROM satisfies the CRC. +- **Driver return code** on every read. +- **Range check** against 10..1200 mbar and -40..+85 C. This is the one that + catches the failure the I2C driver misses entirely: an ADC read taken before + the conversion completes returns 0 (datasheet p.11), and `MS5611::read()` + still reports `MS5611_READ_OK` for it. +- **Stale detection** - bit-identical pressure *and* temperature for 32 + consecutive polls means the part stopped converting while still acknowledging. + +Any failure lights that sensor's LED for 1 second (non-blocking, extended if +further errors follow). After 10 consecutive failures the channel is taken back +through the full reset and PROM handshake, so a sensor that was unplugged and +reconnected recovers on its own. + +The CRC-4 implementation was verified differentially on the host against the +independent NuttX reference over 200000 randomised PROM images. + +## References +- [Barometric Pressure Sensor MS5611](docs/datasheets/ENG_DS_MS5611-01BA03_B3.pdf) +- [STM32F042K6T6 Nucleo](docs/datasheets/DS_stm32f042k6.pdf) +- [STM32 Nucleo-32 boards (MB1180)](https://www.st.com/resource/en/user_manual/um1956-stm32-nucleo32-boards-mb1180-stmicroelectronics.pdf) - UM1956, LED and connector tables diff --git a/docs/datasheets/DB_nucleo-f042k6.pdf b/docs/datasheets/DB_nucleo-f042k6.pdf new file mode 100644 index 0000000..09d5ad6 Binary files /dev/null and b/docs/datasheets/DB_nucleo-f042k6.pdf differ diff --git a/docs/datasheets/DS_stm32f042k6.pdf b/docs/datasheets/DS_stm32f042k6.pdf new file mode 100644 index 0000000..ea4e415 Binary files /dev/null and b/docs/datasheets/DS_stm32f042k6.pdf differ diff --git a/docs/datasheets/ENG_DS_MS5611-01BA03_B3.pdf b/docs/datasheets/ENG_DS_MS5611-01BA03_B3.pdf new file mode 100644 index 0000000..69817d9 Binary files /dev/null and b/docs/datasheets/ENG_DS_MS5611-01BA03_B3.pdf differ diff --git a/docs/wiring-physical.png b/docs/wiring-physical.png new file mode 100644 index 0000000..bb35d5b Binary files /dev/null and b/docs/wiring-physical.png differ diff --git a/docs/wiring-physical.svg b/docs/wiring-physical.svg new file mode 100644 index 0000000..25c9e3d --- /dev/null +++ b/docs/wiring-physical.svg @@ -0,0 +1,330 @@ + + + + + + + + + + + + + + + + + + Physical wiring - NUCLEO-F042K6 + 2 x GY-63 (MS5611) + Board seen from the component side, USB at the top. Header order follows ST UM1956 Table 10. Highlighted pads are the ones this firmware uses. + + + + + + + + + + + + + + + + LD1 + + ST-LINK + ST + + + + + STM32F042 + + + + + + + + + + + + LD2 + + LD3 + + B1 + + CN3 + CN4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + D1 D0 RST + GND D2 D3 + D4 D5 D6 + D7 D8 D9 + D10 D11 D12 + VIN GND + RST 5V + A7 A6 + A5 A4 + A3 A2 + A1 A0 + REF 3V3 + D13 + + + + + + + + + + + + + + + + + + + + PS + SDO + CSB + SDA + SCL + GND + VCC + + + + + + + + + + + MS5611 + + GY-63 #2 - SPI + + + + + + + + + + + + + + + + + + + VCCGND + SCLSDA + CSBSDO + PS + + + + + + + + + + + MS5611 + + GY-63 #1 - I2C + + + + + + + GND rail + 3V3 rail + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 510 R + I2C error + + + + + + + + 510 R + SPI error + + + + + + + + + + + + + + + + + + WIRE COLOURS + + 3V3 + GND + SCL + SDA + LED drive + SCK + MOSI + MISO + CS + + + + NOTES + SPI is split across both headers: MOSI / MISO / CS sit on CN3 + (D11 / D12 / D10) but SCK is CN4 pin 15 (D13). One jumper has + to cross to the far side of the board - that is normal here. + I2C is wired to A5 / A4 on CN4. Solder bridges SB16 / SB18 are + closed from the factory, which puts PB6 / PB7 on those pads. + D5 / D4 on CN3 are the same two nets - either pair works. + Do not power the sensors from 5V. MS5611 runs on 1.8 - 3.6 V. + Both LEDs are external and active high: the GPIO drives the + anode through 510 R. LD3 on the board cannot be used - it + shares PB3 with SPI1 SCK. + A dot means the wire connects to the rail. Wires that merely + cross without a dot are not connected. + Sensor #2 is drawn turned 180 deg so its pad row faces the board - + that is why its pads read PS first and VCC last. + diff --git a/docs/wiring.png b/docs/wiring.png new file mode 100644 index 0000000..94fd3e1 Binary files /dev/null and b/docs/wiring.png differ diff --git a/docs/wiring.svg b/docs/wiring.svg new file mode 100644 index 0000000..7a1ee68 --- /dev/null +++ b/docs/wiring.svg @@ -0,0 +1,226 @@ + + + + + + NUCLEO-F042K6 + 2 x GY-63 (MS5611) - wiring + Logical net diagram. Pins are grouped by function, not by physical header order - see UM1956 Table 10 for the real connector layout. + + + + POWER SOURCE + USB Micro-B to CN1, 5 V from the host PC + ST-LINK V2-1 + on-board 3.3 V LDO + same cable carries the serial VCP + + + + 5 V + serial + + + + +3V3 + + + + + + + NUCLEO-F042K6 + STM32F042K6T6 - board MB1180 + 32 KB flash / 6 KB RAM + + + + + + CN1 + 3V3 + GND + + + + + PB6 / D5 (pad A5) + PB7 / D4 (pad A4) + + + + + + + PB3 / D13 + PB5 / D11 + PA11 / D10 + PB4 / D12 + + + + + PB0 / D3 + PB1 / D6 + + USART2 = PA2 / PA15 to ST-LINK VCP + 115200 8N1 - no USB-serial adapter needed + + + + GY-63 #1 + MS5611-01BA03 - I2C mode + + + + + + + + + VCC + GND + SCL + SDA + CSB + SDO + PS + not connected + + + + GY-63 #2 + MS5611-01BA03 - SPI mode + + + + + + + + + VCC + GND + SCL = SCLK + SDA = SDI / MOSI + CSB = CS + SDO = MISO + PS + + + + + + + + + + + + + + + + + SCL + SDA + + + + + 0x77 + + + + + 3V3 + + + + + + + SCK + MOSI + CS + MISO + + + + + + + + + + + + + + + 510 R + LED - I2C sensor error + + + + + + + + + + 510 R + LED - SPI sensor error + + + + LEGEND + 3.3 V + GND + I2C bus + SPI bus + LED drive (active high) + + + + NOTES + MS5611 is 3.3 V only (1.8 - 3.6 V). + Never feed it 5 V. + GY-63 boards normally carry SDA / SCL + pull-ups - check yours before adding any. + PS high selects I2C, PS low selects SPI. + CSB low on module #1 sets address 0x77 + (BARO_I2C_ADDRESS in include/config.h). + SPI1 must stay on PB3 / PB4 / PB5: bridges + SB16 / SB18 tie PA5 / PA6 to the I2C net. + The Nucleo-32 has a single 3V3 pin - + distribute it over a breadboard rail. + diff --git a/include/README b/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/include/baro_channel.h b/include/baro_channel.h new file mode 100644 index 0000000..d7a893b --- /dev/null +++ b/include/baro_channel.h @@ -0,0 +1,100 @@ +#pragma once + +#include +#include + +#include "config.h" +#include "ms5611_crc.h" + +// +// One validated MS5611 measurement channel. +// +// The class owns everything that is identical for both sensors - reset and +// PROM/CRC handshake, per-poll validation, error accounting and the 1 second +// error LED pulse. Only the five driver calls at the bottom are virtual, and +// they are implemented once for the I2C part and once for the SPI part. +// + +enum class BaroStatus : uint8_t +{ + NotInitialised = 0, + Ok, + InitFailed, // reset/handshake did not complete + PromCrcError, // factory calibration failed its CRC-4 or is implausible + ReadError, // the driver reported a bus/ADC failure + OutOfRange, // values outside the MS5611 operating envelope + Stale, // sensor answers but has stopped producing new samples +}; + +// Short, log-friendly name for a status - never returns NULL. +const char *baroStatusName(BaroStatus status); + + +class BaroChannel +{ +public: + BaroChannel(const char *name, uint8_t ledPin) + : _name(name), _ledPin(ledPin) {} + + // Configures the LED pin and makes the first initialisation attempt. + void begin(uint32_t now); + + // One acquisition cycle: read, validate, update status and LED. + // Blocks for roughly 2x the ADC conversion time of the configured OSR. + void poll(uint32_t now); + + // Releases the error LED once its hold time has elapsed. Cheap, call often. + void updateLed(uint32_t now); + + const char *name() const { return _name; } + BaroStatus status() const { return _status; } + bool isOk() const { return _status == BaroStatus::Ok; } + float pressure() const { return _pressure; } // mbar + float temperature() const { return _temperature; } // degrees C + uint32_t errorCount() const { return _errorCount; } + +protected: + // Not deleted through this type - keeps the vtable free of a destructor + // slot and avoids dragging in operator delete. + ~BaroChannel() = default; + + // Reset the part and apply the configured oversampling. + virtual bool driverBegin() = 0; + // Fill prom[0..7] with the factory calibration words. + virtual void driverReadProm(uint16_t *prom) = 0; + // True when the driver reported MS5611_READ_OK. + virtual bool driverRead() = 0; + virtual float driverPressure() = 0; + virtual float driverTemperature() = 0; + +private: + bool tryInit(uint32_t now); + void fail(BaroStatus status, uint32_t now); + + const char *_name; + uint8_t _ledPin; + + BaroStatus _status = BaroStatus::NotInitialised; + bool _initialised = false; + uint32_t _lastInitAttempt = 0; + + float _pressure = NAN; + float _temperature = NAN; + + uint8_t _repeatCount = 0; + uint8_t _errorStreak = 0; + uint32_t _errorCount = 0; + + bool _ledOn = false; + uint32_t _ledOffAt = 0; +}; + + +// +// The two concrete channels are built in their own translation units on +// purpose: MS5611.h and MS5611_SPI.h each define `enum osr_t` and their own +// MS5611_READ_OK, so including both in one file does not compile. These +// accessors hand out the instances without leaking either driver header. +// +BaroChannel &baroI2cChannel(); +BaroChannel &baroSpiChannel(); diff --git a/include/config.h b/include/config.h new file mode 100644 index 0000000..17f5a1e --- /dev/null +++ b/include/config.h @@ -0,0 +1,120 @@ +#pragma once + +#include + +// +// Project-wide hardware and behaviour configuration. +// +// Board : NUCLEO-F042K6 (STM32F042K6T6, LQFP32, board MB1180) +// 32 KB flash / 6 KB RAM +// Sensor: 2x GY-63 breakout carrying a MS5611-01BA03 +// one on I2C1, one on SPI1 +// +// Pin facts below are taken from ST UM1956 "STM32 Nucleo-32 boards (MB1180)" +// Table 10 + the MB1180 C.2 schematic, cross-checked against the STM32duino +// variant files for NUCLEO_F042K6. +// + +// --------------------------------------------------------------------------- +// I2C sensor ("baro0") +// --------------------------------------------------------------------------- +// STM32duino default Wire pins for this variant: +// PIN_WIRE_SDA = PB7 (Arduino D4, routed to the A4 header pad via SB18) +// PIN_WIRE_SCL = PB6 (Arduino D5, routed to the A5 header pad via SB16) +// Both are the STM32duino defaults, so plain Wire.begin() selects them and no +// explicit setSDA()/setSCL() call is required. +// +// MS5611 I2C address is 1110 11Cx where C is the COMPLEMENT of the CSB pin +// (datasheet p.12), therefore: +// CSB tied to GND -> 0x77 +// CSB tied to VCC -> 0x76 +// Change this if the GY-63 CSB pad is strapped high. +#define BARO_I2C_ADDRESS 0x77 + +// --------------------------------------------------------------------------- +// SPI sensor ("baro1") +// --------------------------------------------------------------------------- +// SPI1 MUST stay on the PB3/PB4/PB5 group on this board. +// +// The alternative SPI1 group (PA5 SCK / PA6 MISO / PA7 MOSI) is NOT usable +// here: with the factory-default solder bridges SB16 and SB18 closed, PA6 is +// tied to the same net as PB6 (I2C SCL) and PA5 is tied to the same net as +// PB7 (I2C SDA). Driving SPI on PA5/PA6 while I2C runs on PB6/PB7 would put +// two peripherals on one net. See UM1956 Table 8 (SB16/SB18). +// +// SCK = PB3 (Arduino D13) <- also the on-board user LED LD3, see below +// MISO = PB4 (Arduino D12) <- connect to sensor SDO +// MOSI = PB5 (Arduino D11) <- connect to sensor SDI +// These three are the STM32duino defaults (core falls back to Arduino pin +// numbers 13/12/11), so plain SPI.begin() selects them. +#define BARO_SPI_CS_PIN PA11 // Arduino D10, core default PIN_SPI_SS + +// MS5611 accepts SPI mode 0 and mode 3, up to 20 MHz (datasheet p.5/p.6). +// The library hardcodes mode 0; 1 MHz is its default and is plenty here. +#define BARO_SPI_CLOCK_HZ 1000000UL + +// --------------------------------------------------------------------------- +// Error indicator LEDs - one per sensor +// --------------------------------------------------------------------------- +// IMPORTANT: the NUCLEO-F042K6 has exactly ONE user-controllable LED, and it +// is unusable for this project: +// +// LD1 (COM, tricolor) - driven by the ST-LINK MCU, not by the target +// LD2 (PWR, red) - hardwired to the power rail, not by the target +// LD3 (user, green) - on PB3 via SB15 + R23, and PB3 is our SPI1 SCK +// +// So both indicators are external LEDs (LED + ~510R to GND) on free GPIOs. +// PB0/PB1 are plain GPIO on this board and collide with nothing we use. +#define LED_BARO_I2C_PIN PB0 // Arduino D3 - error LED for the I2C sensor +#define LED_BARO_SPI_PIN PB1 // Arduino D6 - error LED for the SPI sensor + +// How long an LED stays lit after an error is detected. +#define LED_ERROR_HOLD_MS 1000UL + +// --------------------------------------------------------------------------- +// UART +// --------------------------------------------------------------------------- +// `Serial` on this variant is USART2 (SERIAL_UART_INSTANCE 2) on PA2/PA15, +// which is wired to the ST-LINK Virtual COM Port. No extra wiring needed. +#define UART_BAUD 115200UL + +// Measurement report rate: 10 Hz. +#define REPORT_PERIOD_MS 100UL + +// --------------------------------------------------------------------------- +// Acquisition +// --------------------------------------------------------------------------- +// The driver's read() busy-waits through two conversions, so a poll of both +// sensors is the loop period, and the report deadline can only be evaluated +// on that grid. Keeping the cycle short keeps the 10 Hz output jitter small: +// +// OSR_STANDARD (1024) 2.28 ms/conv -> ~5 ms/sensor -> ~11 ms cycle +// OSR_ULTRA_HIGH (4096) 9.04 ms/conv -> ~19 ms/sensor -> ~37 ms cycle +// +// Conversion times are the datasheet p.3 maxima. OSR 1024 already resolves +// well under a mbar, so it is the better trade here; raise it if resolution +// matters more than tight report timing. +#define BARO_OVERSAMPLING OSR_STANDARD + +// Retry interval for a sensor that failed to initialise. +#define BARO_INIT_RETRY_MS 2000UL + +// Consecutive failed polls after which the channel is torn down and taken +// back through the full reset + PROM/CRC handshake. Covers a sensor that was +// unplugged, browned out, or otherwise lost its calibration constants. +#define BARO_REINIT_AFTER_ERRORS 10 + +// --------------------------------------------------------------------------- +// Validation limits +// --------------------------------------------------------------------------- +// MS5611-01BA03 operating ranges (datasheet p.2/p.4). +#define BARO_PRESSURE_MIN_MBAR 10.0f +#define BARO_PRESSURE_MAX_MBAR 1200.0f +#define BARO_TEMP_MIN_C (-40.0f) +#define BARO_TEMP_MAX_C 85.0f + +// A healthy MS5611 dithers by well under a mbar but never repeats a 24-bit +// reading bit-for-bit many times running. Identical pressure AND temperature +// this many polls in a row means the sensor stopped converting while still +// answering on the bus. +#define BARO_STALE_LIMIT 32 diff --git a/include/ms5611_crc.h b/include/ms5611_crc.h new file mode 100644 index 0000000..aeb5a65 --- /dev/null +++ b/include/ms5611_crc.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +// MS5611 factory PROM CRC-4 (datasheet p.13, algorithm specified in AN520). +// `prom` holds the 8 PROM words as read from the part; word 7 carries the +// stored CRC in its low nibble. The array is restored before returning. +// +// Deliberately free of Arduino headers so it can be built and tested on the +// host against the AN520 reference vector. +bool ms5611PromCrcOk(uint16_t prom[8]); diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..2593a33 --- /dev/null +++ b/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..889638a --- /dev/null +++ b/platformio.ini @@ -0,0 +1,33 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[common] +framework = arduino +lib_deps = + SPI + I2C + https://github.com/RobTillaart/MS5611#0.5.2 + https://github.com/RobTillaart/MS5611_SPI#0.4.3 + + +[env:nucleo_f042k6] +extends = common +platform = ststm32 +board = nucleo_f042k6 +monitor_speed = 115200 +; -flto and -fsingle-precision-constant are load-bearing, not tuning: the +; STM32F042K6 has only 32 KB of flash and the build overflows without them. +; Both MS5611 drivers write their compensation maths with unsuffixed double +; literals, which would otherwise link ~5 KB of double soft-float helpers. +build_flags = + -Wall + -Wextra + -flto + -fsingle-precision-constant diff --git a/src/baro_channel.cpp b/src/baro_channel.cpp new file mode 100644 index 0000000..33aa610 --- /dev/null +++ b/src/baro_channel.cpp @@ -0,0 +1,151 @@ +#include "baro_channel.h" + +const char *baroStatusName(BaroStatus status) +{ + switch (status) + { + case BaroStatus::NotInitialised: return "INIT"; + case BaroStatus::Ok: return "OK"; + case BaroStatus::InitFailed: return "ERR_INIT"; + case BaroStatus::PromCrcError: return "ERR_CRC"; + case BaroStatus::ReadError: return "ERR_READ"; + case BaroStatus::OutOfRange: return "ERR_RANGE"; + case BaroStatus::Stale: return "ERR_STALE"; + } + return "ERR_UNKNOWN"; +} + + +void BaroChannel::begin(uint32_t now) +{ + pinMode(_ledPin, OUTPUT); + digitalWrite(_ledPin, LOW); + _ledOn = false; + + tryInit(now); +} + + +bool BaroChannel::tryInit(uint32_t now) +{ + _lastInitAttempt = now; + _initialised = false; + _repeatCount = 0; + + if (!driverBegin()) + { + fail(BaroStatus::InitFailed, now); + return false; + } + + uint16_t prom[8] = { 0 }; + driverReadProm(prom); + + // An absent part reads back as all-zero (I2C NACK) or all-ones (floating + // MISO). Neither driver rejects 0xFFFF, and an all-zero PROM would satisfy + // the CRC, so the calibration words are screened before the checksum runs. + for (uint8_t i = 1; i <= 6; i++) + { + if (prom[i] == 0x0000 || prom[i] == 0xFFFF) + { + fail(BaroStatus::PromCrcError, now); + return false; + } + } + + if (!ms5611PromCrcOk(prom)) + { + fail(BaroStatus::PromCrcError, now); + return false; + } + + _initialised = true; + _errorStreak = 0; + return true; +} + + +void BaroChannel::poll(uint32_t now) +{ + if (!_initialised) + { + if ((uint32_t)(now - _lastInitAttempt) < BARO_INIT_RETRY_MS) return; + if (!tryInit(now)) return; + } + + if (!driverRead()) + { + fail(BaroStatus::ReadError, now); + return; + } + + const float pressure = driverPressure(); + const float temperature = driverTemperature(); + + // Catches the failure the drivers miss: an ADC read taken before the + // conversion finished returns 0 (datasheet p.11), which the I2C driver + // still reports as MS5611_READ_OK but which lands far outside this envelope. + if (!isfinite(pressure) || !isfinite(temperature) || + pressure < BARO_PRESSURE_MIN_MBAR || pressure > BARO_PRESSURE_MAX_MBAR || + temperature < BARO_TEMP_MIN_C || temperature > BARO_TEMP_MAX_C) + { + _pressure = pressure; + _temperature = temperature; + fail(BaroStatus::OutOfRange, now); + return; + } + + const bool repeated = (pressure == _pressure) && (temperature == _temperature); + + _pressure = pressure; + _temperature = temperature; + + if (repeated) + { + if (_repeatCount < BARO_STALE_LIMIT) _repeatCount++; + } + else + { + _repeatCount = 0; + } + + if (_repeatCount >= BARO_STALE_LIMIT) + { + fail(BaroStatus::Stale, now); + return; + } + + _status = BaroStatus::Ok; + _errorStreak = 0; +} + + +void BaroChannel::fail(BaroStatus status, uint32_t now) +{ + _status = status; + _errorCount++; + + _ledOn = true; + _ledOffAt = now + LED_ERROR_HOLD_MS; + digitalWrite(_ledPin, HIGH); + + if (_errorStreak < BARO_REINIT_AFTER_ERRORS) _errorStreak++; + + if (_errorStreak >= BARO_REINIT_AFTER_ERRORS) + { + _initialised = false; + _errorStreak = 0; + _lastInitAttempt = now; + } +} + + +void BaroChannel::updateLed(uint32_t now) +{ + // Signed difference so the comparison survives the millis() rollover. + if (_ledOn && (int32_t)(now - _ledOffAt) >= 0) + { + _ledOn = false; + digitalWrite(_ledPin, LOW); + } +} diff --git a/src/baro_i2c.cpp b/src/baro_i2c.cpp new file mode 100644 index 0000000..632e790 --- /dev/null +++ b/src/baro_i2c.cpp @@ -0,0 +1,46 @@ +#include +#include + +#include "baro_channel.h" + +namespace { + +class BaroI2cChannel : public BaroChannel +{ +public: + BaroI2cChannel() + : BaroChannel("i2c", LED_BARO_I2C_PIN), _driver(BARO_I2C_ADDRESS, &Wire) {} + +protected: + bool driverBegin() override + { + if (!_busStarted) + { + Wire.begin(); + _busStarted = true; + } + + _driver.setOversampling(BARO_OVERSAMPLING); + + return _driver.begin(); + } + + void driverReadProm(uint16_t *prom) override + { + for (uint8_t i = 0; i < 8; i++) prom[i] = _driver.getProm(i); + } + + bool driverRead() override { return _driver.read() == MS5611_READ_OK; } + float driverPressure() override { return _driver.getPressure(); } + float driverTemperature() override { return _driver.getTemperature(); } + +private: + MS5611 _driver; + bool _busStarted = false; +}; + +BaroI2cChannel instance; + +} // namespace + +BaroChannel &baroI2cChannel() { return instance; } diff --git a/src/baro_spi.cpp b/src/baro_spi.cpp new file mode 100644 index 0000000..ec72cbb --- /dev/null +++ b/src/baro_spi.cpp @@ -0,0 +1,49 @@ +#include +#include + +#include "baro_channel.h" + +namespace { + +class BaroSpiChannel : public BaroChannel +{ +public: + BaroSpiChannel() + : BaroChannel("spi", LED_BARO_SPI_PIN), _driver(BARO_SPI_CS_PIN, &SPI) {} + +protected: + bool driverBegin() override + { + if (!_busStarted) + { + SPI.begin(); + _busStarted = true; + } + + // Both must precede begin(), which re-applies the stored SPI speed and + // then talks to the part. + _driver.setSPIspeed(BARO_SPI_CLOCK_HZ); + _driver.setOversampling(BARO_OVERSAMPLING); + + return _driver.begin(); + } + + void driverReadProm(uint16_t *prom) override + { + for (uint8_t i = 0; i < 8; i++) prom[i] = _driver.getProm(i); + } + + bool driverRead() override { return _driver.read() == MS5611_READ_OK; } + float driverPressure() override { return _driver.getPressure(); } + float driverTemperature() override { return _driver.getTemperature(); } + +private: + MS5611_SPI _driver; + bool _busStarted = false; +}; + +BaroSpiChannel instance; + +} // namespace + +BaroChannel &baroSpiChannel() { return instance; } diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..34a9068 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,93 @@ +#include + +#include "baro_channel.h" +#include "config.h" + +namespace { + +uint32_t nextReportAt = 0; + +// Serial.print(float, digits) resolves to Print::print(double, int), which +// would pull the double-precision soft-float routines into a 32 KB image. +// Formatting from a scaled integer keeps the whole build single-precision. +void printFixed2(float value) +{ + if (!isfinite(value)) + { + Serial.print(F("nan")); + return; + } + + int32_t hundredths = (int32_t)(value * 100.0f + (value < 0.0f ? -0.5f : 0.5f)); + + if (hundredths < 0) + { + Serial.print('-'); + hundredths = -hundredths; + } + + const int32_t fraction = hundredths % 100; + + Serial.print(hundredths / 100); + Serial.print('.'); + if (fraction < 10) Serial.print('0'); + Serial.print(fraction); +} + +void printChannel(BaroChannel &channel) +{ + Serial.print(','); + Serial.print(baroStatusName(channel.status())); + Serial.print(','); + printFixed2(channel.pressure()); + Serial.print(','); + printFixed2(channel.temperature()); +} + +} // namespace + + +void setup() +{ + Serial.begin(UART_BAUD); + + const uint32_t now = millis(); + baroI2cChannel().begin(now); + baroSpiChannel().begin(now); + + Serial.println(); + Serial.println(F("ms,i2c_status,i2c_p_mbar,i2c_t_c,spi_status,spi_p_mbar,spi_t_c")); + + nextReportAt = millis() + REPORT_PERIOD_MS; +} + + +void loop() +{ + uint32_t now = millis(); + + baroI2cChannel().poll(now); + baroSpiChannel().poll(now); + + // poll() blocks for the ADC conversions, so re-read the clock before the + // LED and report deadlines are evaluated. + now = millis(); + + baroI2cChannel().updateLed(now); + baroSpiChannel().updateLed(now); + + if ((int32_t)(now - nextReportAt) < 0) return; + + // Advancing by a fixed period keeps the long-run average at exactly 10 Hz + // instead of drifting by one loop iteration per report. + nextReportAt += REPORT_PERIOD_MS; + + // Unless a stall put us more than a full period behind, in which case the + // grid is re-based rather than emitting a burst of catch-up lines. + if ((int32_t)(now - nextReportAt) >= 0) nextReportAt = now + REPORT_PERIOD_MS; + + Serial.print(now); + printChannel(baroI2cChannel()); + printChannel(baroSpiChannel()); + Serial.println(); +} diff --git a/src/ms5611_crc.cpp b/src/ms5611_crc.cpp new file mode 100644 index 0000000..0fc9d4a --- /dev/null +++ b/src/ms5611_crc.cpp @@ -0,0 +1,27 @@ +#include "ms5611_crc.h" + +bool ms5611PromCrcOk(uint16_t prom[8]) +{ + const uint16_t crcRead = prom[7]; + uint16_t remainder = 0; + + // The 4 CRC bits live in the low nibble of word 7 and must read as zero + // while the remainder is computed, so the whole low byte is masked off. + prom[7] &= 0xFF00; + + for (uint8_t i = 0; i < 16; i++) + { + if (i & 1) remainder ^= (uint16_t)(prom[i >> 1] & 0x00FF); + else remainder ^= (uint16_t)(prom[i >> 1] >> 8); + + for (uint8_t bit = 8; bit > 0; bit--) + { + remainder = (remainder & 0x8000) ? (uint16_t)((remainder << 1) ^ 0x3000) + : (uint16_t)(remainder << 1); + } + } + + prom[7] = crcRead; + + return (uint16_t)((remainder >> 12) & 0x000F) == (uint16_t)(crcRead & 0x000F); +} diff --git a/test/README b/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html