Initial commit
This commit is contained in:
commit
4123391986
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal 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
10
.vscode/extensions.json
vendored
Normal 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
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"C_Cpp.errorSquiggles": "enabled"
|
||||
}
|
||||
34
include/can_manager.h
Normal file
34
include/can_manager.h
Normal 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
146
include/config.h
Normal 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
135
include/mps_sensor.h
Normal 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
25
include/spi_manager.h
Normal 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
36
include/uart_manager.h
Normal 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
46
lib/README
Normal 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
96
platformio.ini
Normal 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
22
readme.md
Normal 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
301
src/can_manager.cpp
Normal 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
168
src/main.cpp
Normal 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
335
src/mps_sensor.cpp
Normal 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
63
src/spi_manager.cpp
Normal 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
199
src/uart_manager.cpp
Normal 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
11
test/README
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user