Initial commit

This commit is contained in:
Gustavo 2026-07-24 16:39:26 +02:00
commit 4123391986
17 changed files with 1635 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch

10
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,10 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"platformio.platformio-ide"
],
"unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack"
]
}

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"C_Cpp.errorSquiggles": "enabled"
}

34
include/can_manager.h Normal file
View File

@ -0,0 +1,34 @@
#ifndef CAN_MANAGER_H
#define CAN_MANAGER_H
#pragma once
#include <stdint.h>
#include <stdbool.h>
#include "config.h"
#include "mps_sensor.h"
#define CAN_CLASSIC_DLC 8
#define CAN_FD_MAX_DLC 64
typedef struct {
uint32_t id;
uint8_t dlc;
uint8_t data[CAN_FD_MAX_DLC];
bool is_fd;
bool brs;
} CanMessage_t;
bool CAN_Init(void);
bool CAN_Send(const CanMessage_t *msg);
bool CAN_Available(void);
bool CAN_Receive(CanMessage_t *msg);
bool CAN_SendH2Status(const MpsSensorData_t *data);
bool CAN_SendH2Alarm(const MpsSensorData_t *data);
bool CAN_SendEnvData(const MpsSensorData_t *data);
void CAN_HandleInterrupt(void);
void CAN_GetErrorCounters(uint8_t *txErr, uint8_t *rxErr);
void CAN_Sleep(void);
void CAN_Wake(void);
#endif // CAN_MANAGER_H

146
include/config.h Normal file
View File

@ -0,0 +1,146 @@
#ifndef CONFIG_H
#define CONFIG_H
#pragma once
#include <stdint.h>
// =====================================================
// HARDWARE REVISION FLAGS
// =====================================================
// Uncomment each line only when the corresponding signal
// is physically routed on the PCB. Undefined flags compile
// out all associated code — no floating-pin side effects.
//
// Current PCB v1 population:
// CAN STBY (pin 18) -> NOT routed. Transceiver controlled via XSTBY in IOCON.
// CAN nINT/nINT1 -> NOT routed. Interrupt events polled in main loop.
// MPS -> 5-pin connector: Tx, Rx, GND, Vin, Vout only.
// No NRST, no digital alarm, no switched power.
// LEDs -> NOT populated.
// #define HW_HAS_CAN_STBY // MCP251863 STBY (pin 18) not routed on v1
// Transceiver standby handled via XSTBY in IOCON
// #define HW_HAS_CAN_INT // nINT not routed in v1 — polled in firmware
// #define HW_HAS_LEDS // LEDs not populated in v1
// =====================================================
// SYSTEM CLOCK
// =====================================================
#define F_CPU_HZ 20000000UL // ATtiny3226 internal 20 MHz
// =====================================================
// UART — MPS Sensor (USART0, DEFAULT mux)
// =====================================================
// PCB routing: PB3 = TX (pin 11), PB2 = RX (pin 12)
// CONFIRMED from ATtiny3226 device header: USART0 DEFAULT = PB[3:0].
// (The ATtiny3226 has only USART0 and USART1 in silicon — no USART2.
// USART1 DEFAULT is PA[4:1]; USART1 ALT1 is PC[3:0]. Neither matches
// PB2/PB3, so USART0 is the only peripheral that fits this wiring.)
// Note: PB3 shares TOSC1 — do NOT fit a 32 kHz crystal.
#define MPS_UART_BAUD_RATE 38400UL // From NevadaNano MPS 5.0 User Manual (Table 1)
#define MPS_UART_TX_PIN PIN_PB3 // Pin 11 — USART0 TXD (DEFAULT mux)
#define MPS_UART_RX_PIN PIN_PB2 // Pin 12 — USART0 RXD (DEFAULT mux)
// Ring-buffer sizes (must be powers of 2)
#define UART_TX_BUF_SIZE 64
#define UART_RX_BUF_SIZE 128
// =====================================================
// UART — Debug monitor (USART1, ALT1 mux)
// =====================================================
// PCB routing: PC2 = TX (pin 17), PC1 = RX (pin 16)
// CONFIRMED from ATtiny3226 device header: USART1 ALT1 = PC[3:0].
// Compiled in only when DEBUG_UART is defined (platformio.ini).
#define DBG_UART_BAUD_RATE 115200UL
#define DBG_UART_TX_PIN PIN_PC2 // Pin 17 — USART1 TXD (ALT1 mux)
#define DBG_UART_RX_PIN PIN_PC1 // Pin 16 — USART1 RXD (ALT1 mux)
// =====================================================
// SPI — MCP251863 CAN controller (SPI0, DEFAULT MUX)
// =====================================================
// PCB routing:
// PA1 = MOSI (pin 20) | PA2 = MISO (pin 1)
// PA3 = SCK (pin 2) | PA4 = CS (pin 5, software)
// No PORTMUX remapping needed — this is the factory default.
#define SPI_MOSI_PIN PIN_PA1 // Pin 20
#define SPI_MISO_PIN PIN_PA2 // Pin 1
#define SPI_SCK_PIN PIN_PA3 // Pin 2
#define SPI_CS_CAN_PIN PIN_PA4 // Pin 5 — software CS
#define SPI_CLOCK_HZ 4000000UL // 4 MHz — within MCP251863 spec
#define SPI_MODE SPI_MODE0 // CPOL=0, CPHA=0
#define SPI_BIT_ORDER MSBFIRST
// =====================================================
// MPS ANALOG OUTPUT — backup ADC reading
// =====================================================
// PCB routing: PC0 = Vout MPS (pin 15) — analog 0-3.3 V
// Represents gas concentration as a voltage. Used as a
// cross-check against the primary UART digital interface.
#define MPS_VOUT_PIN PIN_PC0 // Pin 15 — AIN14
#define MPS_VOUT_ADC_CH 14 // AIN14 maps to PC0
// =====================================================
// CAN — MCP251863
// =====================================================
#define CAN_NOMINAL_BAUD 500000UL // 500 kbit/s nominal
#define CAN_DATA_BAUD 2000000UL // 2 Mbit/s data phase (CAN FD)
#define CAN_TX_FIFO_SIZE 8
#define CAN_RX_FIFO_SIZE 16
// 11-bit standard CAN IDs
#define CAN_ID_H2_STATUS 0x100 // Periodic concentration + alarm level
#define CAN_ID_H2_ALARM 0x101 // Alarm level change event
#define CAN_ID_H2_SENSOR_INFO 0x102 // Sensor version / info
#define CAN_ID_H2_ENV 0x103 // Temperature, pressure, humidity
#define CAN_ID_CMD_REQUEST 0x200 // Incoming command from host
// MCP251863 STBY pin — only if routed (v2+ PCB)
#ifdef HW_HAS_CAN_STBY
#define CAN_STBY_PIN PIN_PB4 // PB4 — drive LOW to enable transceiver
#endif
// MCP251863 nINT pin — only if routed (v2+ PCB)
#ifdef HW_HAS_CAN_INT
#define CAN_INT_PIN PIN_PA5 // PA5 — active-low general interrupt
#endif
// =====================================================
// STATUS LEDs (v2+ PCB only)
// =====================================================
#ifdef HW_HAS_LEDS
#define LED_STATUS_PIN PIN_PA5 // Green — normal operation
#define LED_ALARM_PIN PIN_PA6 // Red — gas alarm
#define LED_FAULT_PIN PIN_PA7 // Yellow — sensor / system fault
#endif
// =====================================================
// TIMING & INTERVALS (milliseconds)
// =====================================================
#define MPS_POLL_INTERVAL_MS 2000 // Matches sensor's 0.5 Hz refresh rate (datasheet Sec 2.1.4)
#define MPS_STARTUP_TIMEOUT_MS 50000 // Startup (31s) + Initialization (12s) + margin (datasheet Fig 6)
#define MPS_RESPONSE_TIMEOUT_MS 200 // Max wait for a single UART reply
#define CAN_TX_INTERVAL_MS 500 // Periodic CAN broadcast period
#define WATCHDOG_TIMEOUT_MS 2000 // Software watchdog kick interval
// =====================================================
// ALARM THRESHOLDS (%LEL)
// =====================================================
#define ALARM_LEVEL_1_LEL 10 // Warning
#define ALARM_LEVEL_2_LEL 25 // Alarm
#define ALARM_LEVEL_3_LEL 50 // High alarm
// =====================================================
// MPS PACKET / PROTOCOL
// =====================================================
#define MPS_PACKET_MAX_PAYLOAD 64
#define MPS_REPLY_HEADER_SIZE 6 // Reply: cmdID(1)+status(1)+length(2)+checksum(2)
#define MPS_REQUEST_HEADER_SIZE 8 // Request: cmdID(2)+length(2)+reserved(2)+checksum(2)
#define MPS_PACKET_HEADER_SIZE MPS_REPLY_HEADER_SIZE // kept for backward compatibility
#define MPS_PACKET_MAX_TOTAL (MPS_REQUEST_HEADER_SIZE + MPS_PACKET_MAX_PAYLOAD)
#define MPS_CRC_POLYNOMIAL 0x1021 // CRC-16/CCITT
#define MPS_CRC_INIT 0xFFFF
#endif // CONFIG_H

135
include/mps_sensor.h Normal file
View File

@ -0,0 +1,135 @@
#ifndef MPS_SENSOR_H
#define MPS_SENSOR_H
#pragma once
#include <stdint.h>
#include <stdbool.h>
#include "config.h"
// =====================================================
// MPS SENSOR COMMAND IDs
// =====================================================
#define MPS_CMD_ANSWER 0x01 // Returns a complete answer in one read
#define MPS_CMD_CONC 0x03 // Returns flammable gas concentration [%LEL]
#define MPS_CMD_ID 0x04 // Returns flammable gas ID
#define MPS_CMD_TEMP 0x21 // Returns ambient temperature (C)
#define MPS_CMD_PRES 0x22 // Returns ambient pressure (kPa)
#define MPS_CMD_REL_HUM 0x23 // Returns ambient relative humidity (%RH)
#define MPS_CMD_ABS_HUM 0x24 // Returns ambient absolute humidity (g/m3)
#define MPS_CMD_STATUS 0x41 // Returns MPS status code
#define MPS_CMD_VERSION 0x42 // Returns SW / HW / protocol versions
#define MPS_CMD_SENSOR_INFO 0x43 // Returns sensor serial number and info
#define MPS_CMD_MEAS 0x61 // Sets sensing mode and concentration unit
#define MPS_CMD_RESET 0x62 // Soft reset — clears all data and states
// =====================================================
// MPS MEASUREMENT MODE (payload for MPS_CMD_MEAS)
// =====================================================
// CONFIRMED from NevadaNano MPS 5.0 User Manual, Table 12.
// Only two modes are documented; there is no "idle" or "single" mode.
#define MPS_MEAS_MODE_CONT 0x2 // MPS_CONT — continuous autonomous mode
#define MPS_MEAS_MODE_STOP 0x3 // MPS_STOP — stop measurement
// CONFIRMED from User Manual, Table 11.
#define MPS_CONC_UNIT_LEL_ISO 0x0 // %LEL per ISO 10156
#define MPS_CONC_UNIT_LEL_IEC 0x2 // %LEL per IEC 60079-20-1
// =====================================================
// MPS STATUS CODES
// =====================================================
#define MPS_STATUS_OK 0x00
#define MPS_STATUS_CRC_FAILED 0x01
#define MPS_STATUS_BAD_PARAMETER 0x02
#define MPS_STATUS_EXECUTION_FAILED 0x03
#define MPS_STATUS_NO_MEMORY 0x04
#define MPS_STATUS_UNKNOWN_COMMAND 0x05
#define MPS_STATUS_INCOMPLETE_COMMAND 0x07
#define MPS_STATUS_HW_ERR_AO 0x20
#define MPS_STATUS_HW_ERR_VDD 0x21
#define MPS_STATUS_HW_ERR_VREF 0x22
#define MPS_STATUS_HW_ENV_XCD_RANGE 0x23
#define MPS_STATUS_HW_ENV_SNSR_MALFUNCTION 0x24
#define MPS_STATUS_HW_ERR_MCU 0x25
#define MPS_STATUS_SENSOR_INITIALIZATION 0x26
#define MPS_STATUS_SENSOR_STARTUP 0x27
#define MPS_STATUS_SENSOR_NEGATIVE 0x30
#define MPS_STATUS_CONDENSATION_DETECTED 0x31
#define MPS_STATUS_HW_SENSOR_MALFUNCTION 0x32
#define MPS_STATUS_GAS_DETECTED_DURING_STARTUP 0x33
#define MPS_STATUS_SLOW_GAS_ACCUMULATION 0x34
#define MPS_STATUS_BREATH_OR_HUMIDITY_SURGE 0x35
#define MPS_STATUS_WATCHDOG_MCU_RESET 0x36
#define MPS_STATUS_HW_ERR_WATCHDOG 0x37
#define MPS_STATUS_HW_ERR_DAC_ADC_XCD_RANGE 0x38
// =====================================================
// ALARM LEVELS
// =====================================================
typedef enum {
MPS_ALARM_NONE = 0,
MPS_ALARM_WARNING = 1,
MPS_ALARM_ALARM = 2,
MPS_ALARM_HIGH = 3
} MpsAlarmLevel_t;
// =====================================================
// PACKET STRUCTURES
// =====================================================
// CONFIRMED from NevadaNano MPS 5.0 User Manual, Tables 2 & 3.
// All multi-byte integers are Little-Endian on the wire.
//
// Request header (8 bytes): CmdID(2) + Length(2) + Reserved(2) + Checksum(2)
// Reply header (6 bytes): CmdID(1) + Status(1) + Length(2) + Checksum(2)
//
// NOTE: CmdID in the REQUEST is 2 bytes (high byte always 0x00) for alignment,
// even though only 1 byte is meaningful. This differs from the REPLY, where
// CmdID is 1 byte. Mixing these up breaks the CRC and the sensor will return
// CRC_FAILED (0x01) or simply not respond as expected.
typedef struct __attribute__((packed)) {
uint16_t cmdID; // Command ID, low byte significant, high byte = 0x00
uint16_t length; // Payload length in bytes (0 if no payload)
uint16_t reserved; // Reserved for future use, zero-filled
uint16_t checksum; // CRC-16/CCITT over entire packet (this field zeroed during calc)
uint8_t payload[MPS_PACKET_MAX_PAYLOAD];
} MpsRequest_t;
typedef struct __attribute__((packed)) {
uint8_t cmdID; // Echo of command ID (1 byte in replies)
uint8_t status; // MPS_STATUS_* code
uint16_t length; // Payload length in bytes (0 if no payload)
uint16_t checksum; // CRC-16/CCITT over entire packet (this field zeroed during calc)
uint8_t payload[MPS_PACKET_MAX_PAYLOAD];
} MpsReply_t;
// =====================================================
// SENSOR DATA
// =====================================================
typedef struct {
uint32_t cycle_count; // Measurement cycle number; compare to detect repeats
float concentration_lel;
uint8_t gas_id;
float temperature_c;
float pressure_kpa;
float rel_humidity_pct;
float abs_humidity_gm3;
uint8_t sensor_status;
MpsAlarmLevel_t alarm_level;
bool data_valid;
} MpsSensorData_t;
// =====================================================
// FUNCTION DECLARATIONS
// =====================================================
bool MPS_Init(void);
bool MPS_Reset(void);
bool MPS_SetMeasurementMode(uint8_t mode, uint8_t unit);
bool MPS_ReadStatus(uint8_t *status);
bool MPS_ReadAll(MpsSensorData_t *data);
bool MPS_ReadConcentration(float *conc_lel);
bool MPS_ReadEnvironmental(MpsSensorData_t *data);
void MPS_EvaluateAlarm(MpsSensorData_t *data);
const char *MPS_StatusString(uint8_t status);
uint16_t MPS_ComputeCRC(const uint8_t *data, uint16_t len);
#endif // MPS_SENSOR_H

25
include/spi_manager.h Normal file
View File

@ -0,0 +1,25 @@
#ifndef SPI_MANAGER_H
#define SPI_MANAGER_H
#pragma once
#include <stdint.h>
#include <stdbool.h>
#include "config.h"
// =====================================================
// SPI MANAGER — SPI0 DEFAULT MUX, MCP251863 link
// =====================================================
// PA1=MOSI | PA2=MISO | PA3=SCK | PA4=CS (software)
// Mode 0,0 — MSB first — up to 4 MHz
// =====================================================
void SPI_Init(void);
void SPI_CS_Assert(void);
void SPI_CS_Deassert(void);
uint8_t SPI_TransferByte(uint8_t txByte);
void SPI_TransferBuffer(const uint8_t *txBuf, uint8_t *rxBuf, uint16_t len);
void SPI_Write(const uint8_t *data, uint16_t len);
void SPI_Read(uint8_t *buf, uint16_t len);
#endif // SPI_MANAGER_H

36
include/uart_manager.h Normal file
View File

@ -0,0 +1,36 @@
#ifndef UART_MANAGER_H
#define UART_MANAGER_H
#pragma once
#include <stdint.h>
#include <stdbool.h>
#include "config.h"
// =====================================================
// UART MANAGER
// =====================================================
// MPS sensor link : USART1 PB3=TX (pin 11) / PB2=RX (pin 12)
// Interrupt-driven ring-buffer, 19200 baud
// Debug output : USART2 PC2=TX (pin 17) / PC1=RX (pin 16)
// Polled TX-only, 115200 baud (DEBUG_UART only)
// =====================================================
// ---- MPS UART (USART1) ----
void UART_Init(void);
void UART_SendByte(uint8_t byte);
void UART_SendBuffer(const uint8_t *buf, uint16_t len);
bool UART_Available(void);
uint8_t UART_ReadByte(void);
uint16_t UART_ReadBuffer(uint8_t *buf, uint16_t maxLen, uint32_t timeoutMs);
void UART_FlushRx(void);
uint16_t UART_RxCount(void);
// ---- DEBUG UART (USART2) — compiled in only when DEBUG_UART defined ----
#ifdef DEBUG_UART
void DBG_UART_Init(void);
void UART_Print(const char *str);
void UART_Printf(const char *fmt, ...);
#endif
#endif // UART_MANAGER_H

46
lib/README Normal file
View File

@ -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 <Foo.h>
#include <Bar.h>
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

96
platformio.ini Normal file
View File

@ -0,0 +1,96 @@
; =====================================================
; PlatformIO configuration — H2 Leak Sensor Controller
; MCU : ATtiny3226 (tinyAVR 2-series)
; Programmer: jtag2updi
; =====================================================
; The "atmelmegaavr" platform already bundles megaTinyCore
; under the hood for ATtiny3226 — do NOT add board_build.core
; or platform_packages overrides; doing so breaks the package
; resolver (KeyError: framework-arduino-megaavr-megtinycore).
;
; If USART2 (or any 2-series-only peripheral) is reported as
; "not declared", update the platform package instead:
; pio pkg update
; pio platform update atmelmegaavr
; This refreshes the underlying board manifests/io headers.
;
; Update upload_port / monitor_port below to match your system:
; Windows : COM3, COM4, ...
; Linux : /dev/ttyUSB0, /dev/ttyACM0
; macOS : /dev/cu.usbserial-*, /dev/cu.usbmodem*
; =====================================================
[platformio]
default_envs = attiny3226_jtag2updi
; -------------------------------------------------------
; Main build + upload environment — jtag2updi programmer
; (Arduino board running the jtag2updi sketch, acting as
; a UPDI programmer connected to PA0 on the target.)
; -------------------------------------------------------
[env:attiny3226_jtag2updi]
platform = atmelmegaavr
framework = arduino
board = ATtiny3226
; 20 MHz internal oscillator
board_build.f_cpu = 20000000L
board_hardware.oscillator = internal
; BOD disabled, EEPROM retained on chip erase
board_hardware.bod = disabled
board_hardware.eesave = yes
; jtag2updi upload
upload_protocol = jtag2updi
upload_port = COM3 ; <-- UPDATE to the jtag2updi adapter's port
upload_speed = 19200 ; jtag2updi default — do not change unless
; you rebuilt the jtag2updi sketch otherwise
; Debug serial monitor (USART2 on PC1/PC2) — separate USB-UART adapter
monitor_port = COM4 ; <-- UPDATE to your debug UART adapter
monitor_speed = 115200
build_flags =
-DDEBUG_UART ; Comment out to disable USART2 debug output
; -------------------------------------------------------
; Alternative environment — SerialUPDI (cheap USB-UART + resistor)
; Switch default_envs above to use this instead.
; -------------------------------------------------------
[env:attiny3226_serialupdi]
platform = atmelmegaavr
framework = arduino
board = ATtiny3226
board_build.f_cpu = 20000000L
board_hardware.oscillator = internal
board_hardware.bod = disabled
board_hardware.eesave = yes
upload_protocol = serialupdi
upload_port = COM3 ; <-- UPDATE
upload_speed = 460800
monitor_port = COM3
monitor_speed = 115200
build_flags =
-DDEBUG_UART
; -------------------------------------------------------
; Fuse-only environment
; Run with: pio run -e set_fuses -t fuses
; -------------------------------------------------------
[env:set_fuses]
platform = atmelmegaavr
framework = arduino
board = ATtiny3226
board_build.f_cpu = 20000000L
board_hardware.oscillator = internal
board_hardware.bod = disabled
board_hardware.eesave = yes
upload_protocol = jtag2updi
upload_port = COM4 ; <-- UPDATE

22
readme.md Normal file
View File

@ -0,0 +1,22 @@
Code for the hardware :
- CVM Mainboard GEN2 202410
### How to flash using UPDI
On Arduino IDE :
1. Install the board : http://drazzy.com/package_drazzy.com_index.json
2. Upload the sketch Jtag2updi to an arduino uno
3. Shunt the reset pin to the ground with a 4.7uF capacitor
4. Wire the AVR (ex : ATTiny3226) via the pin 6 of the arduino uno (programmer) to the UPDI pin of the AVR trough a 470k resistor
Use Tinymegacore to upload to the atttiny using platformio.
### TODO
# Datasheet

301
src/can_manager.cpp Normal file
View File

@ -0,0 +1,301 @@
#include "can_manager.h"
#include "spi_manager.h"
#include "config.h"
#include <Arduino.h>
#include <string.h>
// =====================================================
// MCP251863 REGISTER MAP (subset used here)
// =====================================================
#define MCP_CMD_READ 0x03
#define MCP_CMD_WRITE 0x02
#define MCP_REG_CON 0x000
#define MCP_REG_NBTCFG 0x004
#define MCP_REG_DBTCFG 0x008
#define MCP_REG_TDC 0x00C
#define MCP_REG_IOCON 0xE04
#define MCP_REG_INT 0x01C
#define MCP_REG_INTFLAG 0x020
#define MCP_REG_TXQCON 0x050
#define MCP_REG_FIFOCON(n) (0x05C + (n)*12)
#define MCP_REG_FIFOSTA(n) (0x060 + (n)*12)
#define MCP_REG_FIFOUA(n) (0x064 + (n)*12)
// IOCON register bit masks (byte 0)
// XSTBY routes the controller's internal standby signal directly to the
// integrated transceiver via pin 9 (nINT0/GPIO0/XSTBY) — no external
// STBY GPIO required. Used here because STBY (pin 18) is unrouted on v1 PCB.
#define MCP_IOCON_XSTBY (1UL << 6)
#define MCP_IOCON_HVDETSEL (1UL << 4)
#define MCP_MODE_NORMAL_FD 0x00
#define MCP_MODE_SLEEP 0x01
#define MCP_MODE_CONFIG 0x04
#define MCP_TX_FIFO_IDX 1
#define MCP_RX_FIFO_IDX 2
// =====================================================
// LOW-LEVEL REGISTER ACCESS
// =====================================================
static void writeReg(uint16_t addr, uint32_t value) {
uint8_t tx[6];
tx[0] = (uint8_t)((MCP_CMD_WRITE << 4) | (addr >> 8));
tx[1] = (uint8_t)(addr & 0xFF);
tx[2] = (uint8_t)(value & 0xFF);
tx[3] = (uint8_t)((value >> 8) & 0xFF);
tx[4] = (uint8_t)((value >> 16) & 0xFF);
tx[5] = (uint8_t)((value >> 24) & 0xFF);
SPI_Write(tx, 6);
}
static uint32_t readReg(uint16_t addr) {
uint8_t tx[6] = {0};
uint8_t rx[6] = {0};
tx[0] = (uint8_t)((MCP_CMD_READ << 4) | (addr >> 8));
tx[1] = (uint8_t)(addr & 0xFF);
SPI_CS_Assert();
SPI_TransferBuffer(tx, rx, 6);
SPI_CS_Deassert();
return (uint32_t)rx[2] | ((uint32_t)rx[3] << 8) | ((uint32_t)rx[4] << 16) | ((uint32_t)rx[5] << 24);
}
static void modifyReg(uint16_t addr, uint32_t mask, uint32_t value) {
uint32_t reg = readReg(addr);
reg = (reg & ~mask) | (value & mask);
writeReg(addr, reg);
}
static bool setMode(uint8_t mode) {
modifyReg(MCP_REG_CON, 0x07000000UL, (uint32_t)mode << 24);
uint32_t deadline = millis() + 10;
while (millis() < deadline) {
uint32_t con = readReg(MCP_REG_CON);
if (((con >> 21) & 0x07) == mode) return true;
}
return false;
}
// =====================================================
// PUBLIC API IMPLEMENTATION
// =====================================================
bool CAN_Init(void) {
// External STBY GPIO — only if routed (v2+ PCB). On v1, STBY pin 18 is
// unrouted; standby is instead controlled via XSTBY (configured below).
#ifdef HW_HAS_CAN_STBY
pinMode(CAN_STBY_PIN, OUTPUT);
digitalWrite(CAN_STBY_PIN, LOW);
#endif
// /INT pin — only configure if routed (v2+ PCB). On v1, events are
// caught by polling CAN_HandleInterrupt() every main-loop cycle.
#ifdef HW_HAS_CAN_INT
pinMode(CAN_INT_PIN, INPUT_PULLUP);
#endif
// Reset MCP251863
uint8_t resetCmd[2] = {0x00, 0x00};
SPI_Write(resetCmd, 2);
delay(5);
if (!setMode(MCP_MODE_CONFIG)) return false;
// ---- Configure IOCON: enable XSTBY (required on v1 PCB) ----
// IOCON fields must be written as single-byte SFR writes.
// Byte 0 contains the XSTBY bit (bit 6).
{
uint8_t ioconCmd[3];
ioconCmd[0] = (uint8_t)((MCP_CMD_WRITE << 4) | (MCP_REG_IOCON >> 8));
ioconCmd[1] = (uint8_t)(MCP_REG_IOCON & 0xFF);
ioconCmd[2] = (uint8_t)(MCP_IOCON_XSTBY);
SPI_Write(ioconCmd, 3);
}
// ---- Nominal bit time: 500 kbit/s @ 40 MHz MCP251863 system clock ----
// Register layout: [31:24]=SJW [23:16]=TSEG2 [15:8]=TSEG1 [7:0]=BRP
// BRP=0 -> TQ=25ns. Bit = 1(sync)+(TSEG1+1)+(TSEG2+1) = 80 TQ -> 2000ns = 500 kbit/s
// TSEG1 field=62 (segment=63), TSEG2 field=15 (segment=16), SJW field=3 (segment=4)
// Sample point = (1+63)/80 = 80%
// PREVIOUS BUG: bytes were packed in the wrong order (0x003E0F0F), which actually
// produced BRP=15, TSEG1=15, TSEG2=62 -> 31,250 bit/s instead of 500 kbit/s.
// This is why no traffic appeared on a 500 kbit/s BUSMASTER trace.
writeReg(MCP_REG_NBTCFG, 0x030F3E00UL);
// ---- Data bit time: 2 Mbit/s @ 40 MHz (for CAN FD frames) ----
// TSEG1 field=13 (segment=14), TSEG2 field=3 (segment=4), SJW field=3 (segment=4)
// Bit = 1+14+4 = 19 TQ -> 475ns -> ~2.105 Mbit/s, sample point ~79%
writeReg(MCP_REG_DBTCFG, 0x03030D00UL);
// Transmitter Delay Compensation: auto
writeReg(MCP_REG_TDC, 0x00000B00UL);
// TX FIFO (FIFO 1)
writeReg(MCP_REG_FIFOCON(MCP_TX_FIFO_IDX),
(uint32_t)(CAN_TX_FIFO_SIZE - 1) << 24 | 0x00800080UL);
// RX FIFO (FIFO 2)
writeReg(MCP_REG_FIFOCON(MCP_RX_FIFO_IDX),
(uint32_t)(CAN_RX_FIFO_SIZE - 1) << 24);
// Enable RX interrupt for FIFO 2
writeReg(MCP_REG_INT, 0x00000002UL);
return setMode(MCP_MODE_NORMAL_FD);
}
bool CAN_Send(const CanMessage_t *msg) {
uint32_t ua = readReg(MCP_REG_FIFOUA(MCP_TX_FIFO_IDX));
uint32_t t0 = (msg->id & 0x7FFUL) << 18;
uint32_t t1 = (uint32_t)(msg->dlc & 0x0F);
if (msg->is_fd) {
t1 |= (1UL << 4);
if (msg->brs) t1 |= (1UL << 6);
}
uint8_t txBuf[4 + 4 + CAN_FD_MAX_DLC];
txBuf[0] = (uint8_t)(t0 & 0xFF);
txBuf[1] = (uint8_t)(t0 >> 8);
txBuf[2] = (uint8_t)(t0 >> 16);
txBuf[3] = (uint8_t)(t0 >> 24);
txBuf[4] = (uint8_t)(t1 & 0xFF);
txBuf[5] = (uint8_t)(t1 >> 8);
txBuf[6] = (uint8_t)(t1 >> 16);
txBuf[7] = (uint8_t)(t1 >> 24);
uint8_t dataBytes = (msg->dlc <= CAN_FD_MAX_DLC) ? msg->dlc : CAN_FD_MAX_DLC;
memcpy(&txBuf[8], msg->data, dataBytes);
uint8_t spiCmd[2];
spiCmd[0] = (uint8_t)((MCP_CMD_WRITE << 4) | ((ua >> 8) & 0x0F));
spiCmd[1] = (uint8_t)(ua & 0xFF);
SPI_CS_Assert();
SPI_TransferBuffer(spiCmd, nullptr, 2);
SPI_TransferBuffer(txBuf, nullptr, 8 + dataBytes);
SPI_CS_Deassert();
modifyReg(MCP_REG_FIFOCON(MCP_TX_FIFO_IDX), 0x00000008UL, 0x00000008UL);
return true;
}
bool CAN_Available(void) {
uint32_t sta = readReg(MCP_REG_FIFOSTA(MCP_RX_FIFO_IDX));
return (sta & 0x01) != 0;
}
bool CAN_Receive(CanMessage_t *msg) {
if (!CAN_Available()) return false;
uint32_t ua = readReg(MCP_REG_FIFOUA(MCP_RX_FIFO_IDX));
uint8_t spiCmd[2];
spiCmd[0] = (uint8_t)((MCP_CMD_READ << 4) | ((ua >> 8) & 0x0F));
spiCmd[1] = (uint8_t)(ua & 0xFF);
uint8_t rxBuf[8 + CAN_FD_MAX_DLC];
SPI_CS_Assert();
SPI_TransferBuffer(spiCmd, nullptr, 2);
SPI_TransferBuffer(nullptr, rxBuf, 8 + CAN_FD_MAX_DLC);
SPI_CS_Deassert();
uint32_t t0 = (uint32_t)rxBuf[0] | ((uint32_t)rxBuf[1] << 8) | ((uint32_t)rxBuf[2] << 16) | ((uint32_t)rxBuf[3] << 24);
uint32_t t1 = (uint32_t)rxBuf[4] | ((uint32_t)rxBuf[5] << 8);
msg->id = (t0 >> 18) & 0x7FFUL;
msg->dlc = (uint8_t)(t1 & 0x0F);
msg->is_fd = (t1 >> 4) & 0x01;
msg->brs = (t1 >> 6) & 0x01;
uint8_t dataBytes = (msg->dlc <= CAN_FD_MAX_DLC) ? msg->dlc : CAN_FD_MAX_DLC;
memcpy(msg->data, &rxBuf[8], dataBytes);
modifyReg(MCP_REG_FIFOCON(MCP_RX_FIFO_IDX), 0x00000001UL, 0x00000001UL);
return true;
}
static void packFloat(uint8_t *dst, float val) {
uint32_t raw;
memcpy(&raw, &val, 4);
dst[0] = (uint8_t)(raw >> 24);
dst[1] = (uint8_t)(raw >> 16);
dst[2] = (uint8_t)(raw >> 8);
dst[3] = (uint8_t)(raw);
}
bool CAN_SendH2Status(const MpsSensorData_t *data) {
CanMessage_t msg;
memset(&msg, 0, sizeof(msg));
msg.id = CAN_ID_H2_STATUS;
msg.dlc = 6;
packFloat(&msg.data[0], data->concentration_lel);
msg.data[4] = data->sensor_status;
msg.data[5] = (uint8_t)data->alarm_level;
return CAN_Send(&msg);
}
bool CAN_SendH2Alarm(const MpsSensorData_t *data) {
CanMessage_t msg;
memset(&msg, 0, sizeof(msg));
msg.id = CAN_ID_H2_ALARM;
msg.dlc = 6;
packFloat(&msg.data[0], data->concentration_lel);
msg.data[4] = (uint8_t)data->alarm_level;
msg.data[5] = data->sensor_status;
return CAN_Send(&msg);
}
bool CAN_SendEnvData(const MpsSensorData_t *data) {
CanMessage_t msg;
memset(&msg, 0, sizeof(msg));
msg.id = CAN_ID_H2_ENV;
msg.dlc = 8;
int16_t temp = (int16_t)(data->temperature_c * 10.0f);
msg.data[0] = (uint8_t)(temp >> 8);
msg.data[1] = (uint8_t)(temp & 0xFF);
uint16_t pres = (uint16_t)(data->pressure_kpa * 10.0f);
msg.data[2] = (uint8_t)(pres >> 8);
msg.data[3] = (uint8_t)(pres & 0xFF);
msg.data[4] = (uint8_t)data->rel_humidity_pct;
uint16_t absHum = (uint16_t)(data->abs_humidity_gm3 * 100.0f);
msg.data[5] = (uint8_t)(absHum >> 8);
msg.data[6] = (uint8_t)(absHum & 0xFF);
msg.data[7] = 0x00;
return CAN_Send(&msg);
}
void CAN_HandleInterrupt(void) {
readReg(MCP_REG_INTFLAG);
writeReg(MCP_REG_INTFLAG, 0x00000000UL);
}
void CAN_GetErrorCounters(uint8_t *txErr, uint8_t *rxErr) {
uint32_t reg = readReg(0x034);
*rxErr = (uint8_t)(reg & 0xFF);
*txErr = (uint8_t)((reg >> 8) & 0xFF);
}
void CAN_Sleep(void) {
// With XSTBY enabled in IOCON, sleeping the controller automatically
// drives the integrated transceiver into standby.
setMode(MCP_MODE_SLEEP);
#ifdef HW_HAS_CAN_STBY
digitalWrite(CAN_STBY_PIN, HIGH);
#endif
}
void CAN_Wake(void) {
#ifdef HW_HAS_CAN_STBY
digitalWrite(CAN_STBY_PIN, LOW);
delay(1);
#endif
setMode(MCP_MODE_NORMAL_FD);
}

168
src/main.cpp Normal file
View File

@ -0,0 +1,168 @@
#include <Arduino.h>
#include "config.h"
#include "uart_manager.h"
#include "spi_manager.h"
#include "can_manager.h"
#include "mps_sensor.h"
static MpsSensorData_t g_sensorData;
static MpsAlarmLevel_t g_lastAlarmLevel = MPS_ALARM_NONE;
static uint32_t g_lastPollTime = 0;
static uint32_t g_lastCanTxTime = 0;
static bool g_sensorReady = false;
// =====================================================
// LED CONTROL (compiled out when HW_HAS_LEDS is not defined)
// =====================================================
#ifdef HW_HAS_LEDS
static void updateLEDs(void) {
if (!g_sensorReady) {
digitalWrite(LED_STATUS_PIN, (millis() / 500) & 1);
digitalWrite(LED_ALARM_PIN, LOW);
digitalWrite(LED_FAULT_PIN, LOW);
return;
}
bool hasFault = (g_sensorData.sensor_status != MPS_STATUS_OK) &&
(g_sensorData.sensor_status != MPS_STATUS_SENSOR_STARTUP);
digitalWrite(LED_STATUS_PIN, (!hasFault && g_sensorData.data_valid) ? HIGH : LOW);
digitalWrite(LED_FAULT_PIN, hasFault ? HIGH : LOW);
switch (g_sensorData.alarm_level) {
case MPS_ALARM_WARNING: digitalWrite(LED_ALARM_PIN, (millis() / 1000) & 1); break;
case MPS_ALARM_ALARM: digitalWrite(LED_ALARM_PIN, (millis() / 300) & 1); break;
case MPS_ALARM_HIGH: digitalWrite(LED_ALARM_PIN, HIGH); break;
default: digitalWrite(LED_ALARM_PIN, LOW); break;
}
}
#endif // HW_HAS_LEDS
void setup(void) {
#ifdef HW_HAS_LEDS
pinMode(LED_STATUS_PIN, OUTPUT);
pinMode(LED_ALARM_PIN, OUTPUT);
pinMode(LED_FAULT_PIN, OUTPUT);
digitalWrite(LED_STATUS_PIN, LOW);
digitalWrite(LED_ALARM_PIN, LOW);
digitalWrite(LED_FAULT_PIN, LOW);
#endif
// Initialise peripherals
UART_Init(); // USART1 — MPS sensor (PB3/PB2)
SPI_Init(); // SPI0 — MCP251863 CAN (PA1-PA4)
#ifdef DEBUG_UART
DBG_UART_Init(); // USART2 — debug monitor (PC2/PC1)
#endif
#ifdef DEBUG_UART
UART_Printf("\r\n=== H2 Leak Controller ===\r\n");
UART_Printf("MCU: ATtiny3226 @ %lu Hz\r\n", F_CPU_HZ);
#endif
if (!CAN_Init()) {
#ifdef DEBUG_UART
UART_Print("[ERROR] CAN init failed\r\n");
#endif
while (1) { /* halt */ }
}
#ifdef DEBUG_UART
UART_Print("[OK] CAN init (XSTBY active)\r\n");
#endif
if (!MPS_Init()) {
#ifdef DEBUG_UART
UART_Print("[WARN] MPS init timeout — will retry in loop\r\n");
#endif
} else {
g_sensorReady = true;
#ifdef DEBUG_UART
UART_Print("[OK] MPS sensor ready\r\n");
#endif
}
}
void loop(void) {
uint32_t now = millis();
// ---- Poll MPS sensor ----
if (now - g_lastPollTime >= MPS_POLL_INTERVAL_MS) {
g_lastPollTime = now;
if (!g_sensorReady) {
uint8_t status;
if (MPS_ReadStatus(&status) && status == MPS_STATUS_OK) {
MPS_SetMeasurementMode(MPS_MEAS_MODE_CONT, MPS_CONC_UNIT_LEL_ISO);
g_sensorReady = true;
#ifdef DEBUG_UART
UART_Print("[OK] MPS sensor ready (retry)\r\n");
#endif
}
} else {
bool ok = MPS_ReadAll(&g_sensorData);
if (!ok) {
#ifdef DEBUG_UART
UART_Print("[WARN] MPS read failed\r\n");
#endif
g_sensorData.data_valid = false;
} else {
#ifdef DEBUG_UART
UART_Printf("H2: %.1f%%LEL | Tmp: %.1fC | P: %.1fkPa | RH: %.1f%% | Status: %s\r\n",
g_sensorData.concentration_lel,
g_sensorData.temperature_c,
g_sensorData.pressure_kpa,
g_sensorData.rel_humidity_pct,
MPS_StatusString(g_sensorData.sensor_status));
#endif
if (g_sensorData.alarm_level != g_lastAlarmLevel) {
CAN_SendH2Alarm(&g_sensorData);
g_lastAlarmLevel = g_sensorData.alarm_level;
#ifdef DEBUG_UART
UART_Printf("[ALARM] Level changed -> %d\r\n", g_sensorData.alarm_level);
#endif
}
}
}
}
// ---- Periodic CAN broadcast ----
if (now - g_lastCanTxTime >= CAN_TX_INTERVAL_MS) {
g_lastCanTxTime = now;
if (g_sensorData.data_valid) {
CAN_SendH2Status(&g_sensorData);
CAN_SendEnvData(&g_sensorData);
}
}
// ---- Service CAN controller ----
// v1 PCB: /INT unrouted -> service every loop cycle to clear pending flags.
#ifdef HW_HAS_CAN_INT
if (digitalRead(CAN_INT_PIN) == LOW) {
CAN_HandleInterrupt();
}
#else
CAN_HandleInterrupt();
#endif
// ---- Handle incoming CAN commands ----
while (CAN_Available()) {
CanMessage_t rx;
if (CAN_Receive(&rx)) {
if (rx.id == CAN_ID_CMD_REQUEST && rx.dlc >= 1) {
switch (rx.data[0]) {
case 0x01: CAN_SendH2Status(&g_sensorData); break;
case 0x02: CAN_SendEnvData(&g_sensorData); break;
case 0x03: MPS_Reset(); g_sensorReady = false; break;
default: break;
}
}
}
}
#ifdef HW_HAS_LEDS
updateLEDs();
#endif
}

335
src/mps_sensor.cpp Normal file
View File

@ -0,0 +1,335 @@
#include "mps_sensor.h"
#include "uart_manager.h"
#include "config.h"
#include <Arduino.h>
#include <string.h>
// =====================================================
// CRC-16/CCITT LOOKUP TABLE
// =====================================================
// CONFIRMED verbatim from NevadaNano MPS 5.0 User Manual, Section 2.1.3.
// Algorithm: 16-bit CRC CCITT, start value 0xFFFF, MSB-first table-driven.
// Verified against all four worked examples in the datasheet's command
// table (STATUS, ANSWER, RESET, MEAS) — all match exactly.
static const uint16_t crc_table[256] = {
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0,
};
uint16_t MPS_ComputeCRC(const uint8_t *data, uint16_t len) {
uint16_t crc = MPS_CRC_INIT;
for (uint16_t i = 0; i < len; i++) {
crc = (uint16_t)((crc << 8) ^ crc_table[(uint8_t)((crc >> 8) ^ data[i])]);
}
return crc;
}
// =====================================================
// REQUEST BUILDING — CONFIRMED wire format (Little-Endian)
// =====================================================
// Header (8 bytes): CmdID(2,LE) + Length(2,LE) + Reserved(2,LE) + Checksum(2,LE)
// Checksum is computed over the ENTIRE packet (header+payload) with the
// checksum field itself zero-filled, then the result is written back in.
static void sendRequest(uint8_t cmdID, const uint8_t *payload, uint16_t payloadLen) {
uint8_t raw[MPS_PACKET_MAX_TOTAL];
raw[0] = cmdID; // CmdID low byte
raw[1] = 0x00; // CmdID high byte (always 0 — only 1 byte meaningful)
raw[2] = (uint8_t)(payloadLen & 0xFF); // Length LSB
raw[3] = (uint8_t)(payloadLen >> 8); // Length MSB
raw[4] = 0x00; // Reserved LSB
raw[5] = 0x00; // Reserved MSB
raw[6] = 0x00; // Checksum placeholder LSB (zeroed for calc)
raw[7] = 0x00; // Checksum placeholder MSB (zeroed for calc)
if (payload && payloadLen > 0) {
memcpy(&raw[MPS_REQUEST_HEADER_SIZE], payload, payloadLen);
}
uint16_t totalLen = MPS_REQUEST_HEADER_SIZE + payloadLen;
uint16_t crc = MPS_ComputeCRC(raw, totalLen);
// Write CRC back in little-endian, overwriting the placeholder
raw[6] = (uint8_t)(crc & 0xFF);
raw[7] = (uint8_t)(crc >> 8);
UART_SendBuffer(raw, totalLen);
}
// =====================================================
// REPLY PARSING — CONFIRMED wire format (Little-Endian)
// =====================================================
// Header (6 bytes): CmdID(1) + Status(1) + Length(2,LE) + Checksum(2,LE)
static bool receiveReply(uint8_t expectedCmdID, MpsReply_t *reply) {
uint8_t raw[MPS_PACKET_MAX_TOTAL];
uint16_t got = UART_ReadBuffer(raw, MPS_REPLY_HEADER_SIZE, MPS_RESPONSE_TIMEOUT_MS);
if (got < MPS_REPLY_HEADER_SIZE) return false; // Timeout
uint16_t payloadLen = (uint16_t)raw[2] | ((uint16_t)raw[3] << 8); // LE
if (payloadLen > MPS_PACKET_MAX_PAYLOAD) return false; // Sanity check
got = UART_ReadBuffer(&raw[MPS_REPLY_HEADER_SIZE], payloadLen, MPS_RESPONSE_TIMEOUT_MS);
if (got < payloadLen) return false;
uint16_t rxCRC = (uint16_t)raw[4] | ((uint16_t)raw[5] << 8); // LE, as received
// Recompute CRC with the checksum field zeroed, per datasheet
uint8_t crcBuf[MPS_PACKET_MAX_TOTAL];
memcpy(crcBuf, raw, MPS_REPLY_HEADER_SIZE + payloadLen);
crcBuf[4] = 0x00;
crcBuf[5] = 0x00;
uint16_t calcCRC = MPS_ComputeCRC(crcBuf, MPS_REPLY_HEADER_SIZE + payloadLen);
if (rxCRC != calcCRC) return false;
reply->cmdID = raw[0];
reply->status = raw[1];
reply->length = payloadLen;
reply->checksum = rxCRC;
if (payloadLen > 0) memcpy(reply->payload, &raw[MPS_REPLY_HEADER_SIZE], payloadLen);
return (reply->cmdID == expectedCmdID) && (reply->status == MPS_STATUS_OK);
}
// =====================================================
// LITTLE-ENDIAN FIELD PARSING
// =====================================================
// CONFIRMED: "All integer values (16/32-bit) are Little Endian. Floating
// point numbers are IEEE 754." (Section 2.1.1) — same byte order applies
// to floats since they are just 4-byte fields transmitted LSB-first.
static float parseFloatLE(const uint8_t *p) {
uint32_t raw = (uint32_t)p[0] | ((uint32_t)p[1] << 8)
| ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
float f;
memcpy(&f, &raw, sizeof(f));
return f;
}
static uint32_t parseU32LE(const uint8_t *p) {
return (uint32_t)p[0] | ((uint32_t)p[1] << 8)
| ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}
bool MPS_Init(void) {
// MPS 5-pin sensor (Tx, Rx, GND, Vin, Vout) — no NRST, no power switch,
// no digital alarm pin on this PCB revision. Sensor is always powered;
// all control happens over UART.
//
// Per datasheet Section 2.1.4: wait ~3s for POST, then verify comms
// with a STATUS read before starting continuous measurement.
delay(3000);
uint32_t deadline = millis() + MPS_STARTUP_TIMEOUT_MS;
uint8_t status = MPS_STATUS_SENSOR_STARTUP;
bool gotResponse = false;
do {
gotResponse = MPS_ReadStatus(&status);
if (gotResponse && status == MPS_STATUS_OK) break;
delay(500);
} while (millis() < deadline);
if (!gotResponse) return false; // No response at all — comms failure
// Per datasheet: STARTUP/INITIALIZATION are normal transient states.
if (status != MPS_STATUS_OK &&
status != MPS_STATUS_SENSOR_STARTUP &&
status != MPS_STATUS_SENSOR_INITIALIZATION) {
return false;
}
// Start continuous measurement, ISO %LEL units.
// Payload byte = (unit << 4) | mode, per datasheet Table 10.
return MPS_SetMeasurementMode(MPS_MEAS_MODE_CONT, MPS_CONC_UNIT_LEL_ISO);
}
bool MPS_Reset(void) {
UART_FlushRx();
sendRequest(MPS_CMD_RESET, nullptr, 0);
delay(100);
MpsReply_t reply;
return receiveReply(MPS_CMD_RESET, &reply);
}
bool MPS_SetMeasurementMode(uint8_t mode, uint8_t unit) {
// CONFIRMED Table 10: single payload byte, Conc.Unit in bits[7:4], Mode in bits[3:0].
uint8_t payload = (uint8_t)(((unit & 0x0F) << 4) | (mode & 0x0F));
UART_FlushRx();
sendRequest(MPS_CMD_MEAS, &payload, 1);
MpsReply_t reply;
return receiveReply(MPS_CMD_MEAS, &reply);
}
bool MPS_ReadStatus(uint8_t *status) {
UART_FlushRx();
sendRequest(MPS_CMD_STATUS, nullptr, 0);
MpsReply_t reply;
// NOTE: receiveReply() only returns true when reply.status == MPS_STATUS_OK.
// For the STATUS command itself, a non-OK status is still a *valid* reply
// (it's telling us the sensor's real status) — so we must read raw fields
// directly rather than rely on receiveReply()'s success condition.
uint8_t raw[MPS_PACKET_MAX_TOTAL];
uint16_t got = UART_ReadBuffer(raw, MPS_REPLY_HEADER_SIZE, MPS_RESPONSE_TIMEOUT_MS);
if (got < MPS_REPLY_HEADER_SIZE) return false;
uint16_t payloadLen = (uint16_t)raw[2] | ((uint16_t)raw[3] << 8);
if (payloadLen > MPS_PACKET_MAX_PAYLOAD) return false;
got = UART_ReadBuffer(&raw[MPS_REPLY_HEADER_SIZE], payloadLen, MPS_RESPONSE_TIMEOUT_MS);
if (got < payloadLen) return false;
uint16_t rxCRC = (uint16_t)raw[4] | ((uint16_t)raw[5] << 8);
uint8_t crcBuf[MPS_PACKET_MAX_TOTAL];
memcpy(crcBuf, raw, MPS_REPLY_HEADER_SIZE + payloadLen);
crcBuf[4] = 0x00;
crcBuf[5] = 0x00;
uint16_t calcCRC = MPS_ComputeCRC(crcBuf, MPS_REPLY_HEADER_SIZE + payloadLen);
if (rxCRC != calcCRC) return false;
if (raw[0] != MPS_CMD_STATUS) return false;
// Per Table 6: STATUS response payload length is 1 byte = the status code itself.
*status = (payloadLen >= 1) ? raw[MPS_REPLY_HEADER_SIZE] : raw[1];
(void)reply;
return true;
}
bool MPS_ReadConcentration(float *conc_lel) {
UART_FlushRx();
sendRequest(MPS_CMD_CONC, nullptr, 0);
MpsReply_t reply;
if (!receiveReply(MPS_CMD_CONC, &reply)) return false;
if (reply.length < 4) return false;
*conc_lel = parseFloatLE(reply.payload);
return true;
}
bool MPS_ReadEnvironmental(MpsSensorData_t *data) {
MpsReply_t reply;
UART_FlushRx();
sendRequest(MPS_CMD_TEMP, nullptr, 0);
if (receiveReply(MPS_CMD_TEMP, &reply) && reply.length >= 4)
data->temperature_c = parseFloatLE(reply.payload);
UART_FlushRx();
sendRequest(MPS_CMD_PRES, nullptr, 0);
if (receiveReply(MPS_CMD_PRES, &reply) && reply.length >= 4)
data->pressure_kpa = parseFloatLE(reply.payload);
UART_FlushRx();
sendRequest(MPS_CMD_REL_HUM, nullptr, 0);
if (receiveReply(MPS_CMD_REL_HUM, &reply) && reply.length >= 4)
data->rel_humidity_pct = parseFloatLE(reply.payload);
UART_FlushRx();
sendRequest(MPS_CMD_ABS_HUM, nullptr, 0);
if (receiveReply(MPS_CMD_ABS_HUM, &reply) && reply.length >= 4)
data->abs_humidity_gm3 = parseFloatLE(reply.payload);
return true;
}
bool MPS_ReadAll(MpsSensorData_t *data) {
UART_FlushRx();
sendRequest(MPS_CMD_ANSWER, nullptr, 0);
MpsReply_t reply;
if (!receiveReply(MPS_CMD_ANSWER, &reply)) {
data->data_valid = false;
return false;
}
// CONFIRMED ANSWER payload layout (28 bytes total), all Little-Endian,
// per datasheet Section 2.1.6, Command 0x01:
// [0:4] CYCLE_COUNT uint32
// [4:8] CONC float32
// [8:12] ID uint32
// [12:16] TEMP float32
// [16:20] PRESSURE float32
// [20:24] REL_HUM float32
// [24:28] ABS_HUM float32
if (reply.length < 28) {
data->data_valid = false;
return false;
}
data->cycle_count = parseU32LE(&reply.payload[0]);
data->concentration_lel = parseFloatLE(&reply.payload[4]);
data->gas_id = (uint8_t)parseU32LE(&reply.payload[8]);
data->temperature_c = parseFloatLE(&reply.payload[12]);
data->pressure_kpa = parseFloatLE(&reply.payload[16]);
data->rel_humidity_pct = parseFloatLE(&reply.payload[20]);
data->abs_humidity_gm3 = parseFloatLE(&reply.payload[24]);
data->sensor_status = reply.status;
data->data_valid = true;
MPS_EvaluateAlarm(data);
return true;
}
void MPS_EvaluateAlarm(MpsSensorData_t *data) {
float c = data->concentration_lel;
if (c >= ALARM_LEVEL_3_LEL) data->alarm_level = MPS_ALARM_HIGH;
else if (c >= ALARM_LEVEL_2_LEL) data->alarm_level = MPS_ALARM_ALARM;
else if (c >= ALARM_LEVEL_1_LEL) data->alarm_level = MPS_ALARM_WARNING;
else data->alarm_level = MPS_ALARM_NONE;
}
const char *MPS_StatusString(uint8_t status) {
switch (status) {
case MPS_STATUS_OK: return "OK";
case MPS_STATUS_CRC_FAILED: return "CRC Failed";
case MPS_STATUS_BAD_PARAMETER: return "Bad Parameter";
case MPS_STATUS_EXECUTION_FAILED: return "Execution Failed";
case MPS_STATUS_NO_MEMORY: return "No Memory";
case MPS_STATUS_UNKNOWN_COMMAND: return "Unknown Command";
case MPS_STATUS_INCOMPLETE_COMMAND: return "Incomplete Command";
case MPS_STATUS_HW_ERR_AO: return "HW Error: Analog Out";
case MPS_STATUS_HW_ERR_VDD: return "HW Error: VDD";
case MPS_STATUS_HW_ERR_VREF: return "HW Error: VREF";
case MPS_STATUS_HW_ENV_XCD_RANGE: return "Env Sensor OOR";
case MPS_STATUS_HW_ENV_SNSR_MALFUNCTION: return "Env Sensor Malfunction";
case MPS_STATUS_HW_ERR_MCU: return "HW Error: MCU";
case MPS_STATUS_SENSOR_INITIALIZATION: return "Sensor Initialising";
case MPS_STATUS_SENSOR_STARTUP: return "Sensor Startup";
case MPS_STATUS_SENSOR_NEGATIVE: return "Sensor Negative";
case MPS_STATUS_CONDENSATION_DETECTED: return "Condensation";
case MPS_STATUS_HW_SENSOR_MALFUNCTION: return "Sensor Malfunction";
case MPS_STATUS_GAS_DETECTED_DURING_STARTUP: return "Gas at Startup";
case MPS_STATUS_SLOW_GAS_ACCUMULATION: return "Slow Gas Accumulation";
case MPS_STATUS_BREATH_OR_HUMIDITY_SURGE: return "Breath/Humidity Surge";
case MPS_STATUS_WATCHDOG_MCU_RESET: return "Watchdog Reset";
case MPS_STATUS_HW_ERR_WATCHDOG: return "HW Error: Watchdog";
case MPS_STATUS_HW_ERR_DAC_ADC_XCD_RANGE: return "DAC/ADC Range Exceeded";
default: return "Unknown Status";
}
}

63
src/spi_manager.cpp Normal file
View File

@ -0,0 +1,63 @@
#include "spi_manager.h"
#include <Arduino.h>
#include <avr/io.h>
// =====================================================
// SPI MANAGER — SPI0 DEFAULT MUX
// PA1=MOSI (pin 20) PA2=MISO (pin 1)
// PA3=SCK (pin 2) PA4=CS (pin 5, software)
// No PORTMUX remapping required.
// =====================================================
void SPI_Init(void) {
pinMode(SPI_MOSI_PIN, OUTPUT);
pinMode(SPI_MISO_PIN, INPUT);
pinMode(SPI_SCK_PIN, OUTPUT);
pinMode(SPI_CS_CAN_PIN, OUTPUT);
digitalWrite(SPI_CS_CAN_PIN, HIGH); // CS inactive
// Default SPI0 MUX is already PA1/PA2/PA3.
PORTMUX.SPIROUTEA = PORTMUX_SPI0_DEFAULT_gc;
SPI0.CTRLA = SPI_MASTER_bm
| SPI_PRESC_DIV4_gc; // 20 MHz / 4 = 5 MHz
SPI0.CTRLB = SPI_MODE_0_gc
| SPI_SSD_bm; // Software SS
SPI0.CTRLA |= SPI_ENABLE_bm;
}
void SPI_CS_Assert(void) {
digitalWrite(SPI_CS_CAN_PIN, LOW);
}
void SPI_CS_Deassert(void) {
digitalWrite(SPI_CS_CAN_PIN, HIGH);
}
uint8_t SPI_TransferByte(uint8_t txByte) {
SPI0.DATA = txByte;
while (!(SPI0.INTFLAGS & SPI_IF_bm)) { /* wait */ }
return SPI0.DATA;
}
void SPI_TransferBuffer(const uint8_t *txBuf, uint8_t *rxBuf, uint16_t len) {
for (uint16_t i = 0; i < len; i++) {
uint8_t tx = (txBuf != nullptr) ? txBuf[i] : 0x00;
uint8_t rx = SPI_TransferByte(tx);
if (rxBuf != nullptr) rxBuf[i] = rx;
}
}
void SPI_Write(const uint8_t *data, uint16_t len) {
SPI_CS_Assert();
SPI_TransferBuffer(data, nullptr, len);
SPI_CS_Deassert();
}
void SPI_Read(uint8_t *buf, uint16_t len) {
SPI_CS_Assert();
SPI_TransferBuffer(nullptr, buf, len);
SPI_CS_Deassert();
}

199
src/uart_manager.cpp Normal file
View File

@ -0,0 +1,199 @@
#include "uart_manager.h"
#include <Arduino.h>
#include <avr/io.h>
#include <avr/interrupt.h>
#include <stdarg.h>
#include <stdio.h>
// =====================================================
// CONFIRMED PERIPHERAL MAPPING (from ATtiny3226 device header)
// =====================================================
// The ATtiny3226 has only USART0 and USART1 in silicon (no USART2).
// USART0 DEFAULT = PB[3:0] USART0 ALT1 = PA[4:1]
// USART1 DEFAULT = PA[4:1] USART1 ALT1 = PC[3:0]
//
// PCB wiring:
// MPS sensor -> PB3 (TX) / PB2 (RX) => USART0, DEFAULT mux (no PORTMUX write needed)
// Debug link -> PC2 (TX) / PC1 (RX) => USART1, ALT1 mux (PORTMUX write required)
// =====================================================
// =====================================================
// RING BUFFER IMPLEMENTATION
// =====================================================
typedef struct {
uint8_t buf[UART_TX_BUF_SIZE];
volatile uint8_t head;
volatile uint8_t tail;
} TxRingBuf_t;
typedef struct {
uint8_t buf[UART_RX_BUF_SIZE];
volatile uint8_t head;
volatile uint8_t tail;
} RxRingBuf_t;
// MPS UART (USART0) ring buffers
static TxRingBuf_t s_mps_tx;
static RxRingBuf_t s_mps_rx;
#define TX_MASK (UART_TX_BUF_SIZE - 1)
#define RX_MASK (UART_RX_BUF_SIZE - 1)
static inline bool txBuf_full(void) { return ((s_mps_tx.head + 1) & TX_MASK) == s_mps_tx.tail; }
static inline bool txBuf_empty(void) { return s_mps_tx.head == s_mps_tx.tail; }
static inline bool rxBuf_empty(void) { return s_mps_rx.head == s_mps_rx.tail; }
// =====================================================
// USART0 ISRs (MPS sensor link — PB3 TX / PB2 RX, DEFAULT mux)
// =====================================================
ISR(USART0_DRE_vect) {
if (!txBuf_empty()) {
USART0.TXDATAL = s_mps_tx.buf[s_mps_tx.tail];
s_mps_tx.tail = (s_mps_tx.tail + 1) & TX_MASK;
} else {
USART0.CTRLA &= ~USART_DREIE_bm;
}
}
ISR(USART0_RXC_vect) {
uint8_t status = USART0.RXDATAH; // Read status BEFORE data (clears flags)
uint8_t data = USART0.RXDATAL;
if (status & (USART_FERR_bm | USART_PERR_bm | USART_BUFOVF_bm)) {
return;
}
uint8_t next = (s_mps_rx.head + 1) & RX_MASK;
if (next != s_mps_rx.tail) {
s_mps_rx.buf[s_mps_rx.head] = data;
s_mps_rx.head = next;
}
}
// =====================================================
// MPS UART PUBLIC API (USART0, DEFAULT mux — no PORTMUX write needed)
// =====================================================
void UART_Init(void) {
pinMode(MPS_UART_TX_PIN, OUTPUT);
pinMode(MPS_UART_RX_PIN, INPUT);
// USART0 DEFAULT mux is PB[3:0] — this is the factory default,
// but set it explicitly in case something else changed it.
PORTMUX.USARTROUTEA &= ~PORTMUX_USART0_gm;
PORTMUX.USARTROUTEA |= PORTMUX_USART0_DEFAULT_gc;
USART0.BAUD = (uint16_t)((4UL * F_CPU_HZ) / MPS_UART_BAUD_RATE);
USART0.CTRLC = USART_CMODE_ASYNCHRONOUS_gc
| USART_PMODE_DISABLED_gc
| USART_SBMODE_1BIT_gc
| USART_CHSIZE_8BIT_gc;
USART0.CTRLA = USART_RXCIE_bm;
USART0.CTRLB = USART_RXEN_bm | USART_TXEN_bm;
sei();
}
void UART_SendByte(uint8_t byte) {
while (txBuf_full()) { /* spin */ }
uint8_t sreg = SREG;
cli();
s_mps_tx.buf[s_mps_tx.head] = byte;
s_mps_tx.head = (s_mps_tx.head + 1) & TX_MASK;
USART0.CTRLA |= USART_DREIE_bm;
SREG = sreg;
}
void UART_SendBuffer(const uint8_t *buf, uint16_t len) {
for (uint16_t i = 0; i < len; i++) UART_SendByte(buf[i]);
}
bool UART_Available(void) {
return !rxBuf_empty();
}
uint8_t UART_ReadByte(void) {
while (rxBuf_empty()) { /* wait */ }
uint8_t byte = s_mps_rx.buf[s_mps_rx.tail];
uint8_t sreg = SREG;
cli();
s_mps_rx.tail = (s_mps_rx.tail + 1) & RX_MASK;
SREG = sreg;
return byte;
}
uint16_t UART_ReadBuffer(uint8_t *buf, uint16_t maxLen, uint32_t timeoutMs) {
uint16_t count = 0;
uint32_t deadline = millis() + timeoutMs;
while (count < maxLen) {
if (UART_Available()) buf[count++] = UART_ReadByte();
else if (millis() >= deadline) break;
}
return count;
}
void UART_FlushRx(void) {
uint8_t sreg = SREG;
cli();
s_mps_rx.head = s_mps_rx.tail = 0;
SREG = sreg;
}
uint16_t UART_RxCount(void) {
return (uint16_t)((s_mps_rx.head - s_mps_rx.tail) & RX_MASK);
}
// =====================================================
// DEBUG UART (USART1, ALT1 mux — PC2 TX / PC1 RX)
// =====================================================
// Compiled in only when DEBUG_UART is defined.
// Polled, TX-only — fine for debug, not for high throughput.
//
// USART1 ALT1 must be selected via PORTMUX.USARTROUTEA before use,
// otherwise USART1 defaults to PA[4:1] (which is occupied by SPI).
// =====================================================
#ifdef DEBUG_UART
void DBG_UART_Init(void) {
pinMode(DBG_UART_TX_PIN, OUTPUT);
pinMode(DBG_UART_RX_PIN, INPUT);
// Route USART1 to its ALT1 position: PC[3:0] (TX=PC2, RX=PC1)
PORTMUX.USARTROUTEA &= ~PORTMUX_USART1_gm;
PORTMUX.USARTROUTEA |= PORTMUX_USART1_ALT1_gc;
USART1.BAUD = (uint16_t)((4UL * F_CPU_HZ) / DBG_UART_BAUD_RATE);
USART1.CTRLC = USART_CMODE_ASYNCHRONOUS_gc
| USART_PMODE_DISABLED_gc
| USART_SBMODE_1BIT_gc
| USART_CHSIZE_8BIT_gc;
USART1.CTRLB = USART_TXEN_bm; // TX only for debug
}
static void dbg_send_byte(uint8_t b) {
while (!(USART1.STATUS & USART_DREIF_bm)) { /* wait for empty */ }
USART1.TXDATAL = b;
}
void UART_Print(const char *str) {
while (*str) dbg_send_byte((uint8_t)*str++);
}
void UART_Printf(const char *fmt, ...) {
char tmp[80];
va_list args;
va_start(args, fmt);
vsnprintf(tmp, sizeof(tmp), fmt, args);
va_end(args);
UART_Print(tmp);
}
#endif // DEBUG_UART

11
test/README Normal file
View File

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