From 41233919869dc5d5c76d69326c02ae117003814d Mon Sep 17 00:00:00 2001 From: Gustavo Date: Fri, 24 Jul 2026 16:39:26 +0200 Subject: [PATCH] Initial commit --- .gitignore | 5 + .vscode/extensions.json | 10 ++ .vscode/settings.json | 3 + include/can_manager.h | 34 ++++ include/config.h | 146 +++++++++++++++++ include/mps_sensor.h | 135 ++++++++++++++++ include/spi_manager.h | 25 +++ include/uart_manager.h | 36 +++++ lib/README | 46 ++++++ platformio.ini | 96 ++++++++++++ readme.md | 22 +++ src/can_manager.cpp | 301 ++++++++++++++++++++++++++++++++++++ src/main.cpp | 168 ++++++++++++++++++++ src/mps_sensor.cpp | 335 ++++++++++++++++++++++++++++++++++++++++ src/spi_manager.cpp | 63 ++++++++ src/uart_manager.cpp | 199 ++++++++++++++++++++++++ test/README | 11 ++ 17 files changed, 1635 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 include/can_manager.h create mode 100644 include/config.h create mode 100644 include/mps_sensor.h create mode 100644 include/spi_manager.h create mode 100644 include/uart_manager.h create mode 100644 lib/README create mode 100644 platformio.ini create mode 100644 readme.md create mode 100644 src/can_manager.cpp create mode 100644 src/main.cpp create mode 100644 src/mps_sensor.cpp create mode 100644 src/spi_manager.cpp create mode 100644 src/uart_manager.cpp create mode 100644 test/README diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/.vscode/extensions.json @@ -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" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..fc4c15b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "C_Cpp.errorSquiggles": "enabled" +} \ No newline at end of file diff --git a/include/can_manager.h b/include/can_manager.h new file mode 100644 index 0000000..8b9e840 --- /dev/null +++ b/include/can_manager.h @@ -0,0 +1,34 @@ +#ifndef CAN_MANAGER_H +#define CAN_MANAGER_H + +#pragma once + +#include +#include +#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 diff --git a/include/config.h b/include/config.h new file mode 100644 index 0000000..6320eaf --- /dev/null +++ b/include/config.h @@ -0,0 +1,146 @@ +#ifndef CONFIG_H +#define CONFIG_H + +#pragma once + +#include + +// ===================================================== +// 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 diff --git a/include/mps_sensor.h b/include/mps_sensor.h new file mode 100644 index 0000000..6454862 --- /dev/null +++ b/include/mps_sensor.h @@ -0,0 +1,135 @@ +#ifndef MPS_SENSOR_H +#define MPS_SENSOR_H + +#pragma once + +#include +#include +#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 diff --git a/include/spi_manager.h b/include/spi_manager.h new file mode 100644 index 0000000..b2a0ab1 --- /dev/null +++ b/include/spi_manager.h @@ -0,0 +1,25 @@ +#ifndef SPI_MANAGER_H +#define SPI_MANAGER_H + +#pragma once + +#include +#include +#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 diff --git a/include/uart_manager.h b/include/uart_manager.h new file mode 100644 index 0000000..82dfd92 --- /dev/null +++ b/include/uart_manager.h @@ -0,0 +1,36 @@ +#ifndef UART_MANAGER_H +#define UART_MANAGER_H + +#pragma once + +#include +#include +#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 diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..2593a33 --- /dev/null +++ b/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..d13b17e --- /dev/null +++ b/platformio.ini @@ -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 diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..70e8019 --- /dev/null +++ b/readme.md @@ -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 + + diff --git a/src/can_manager.cpp b/src/can_manager.cpp new file mode 100644 index 0000000..e1474df --- /dev/null +++ b/src/can_manager.cpp @@ -0,0 +1,301 @@ +#include "can_manager.h" +#include "spi_manager.h" +#include "config.h" +#include +#include + +// ===================================================== +// 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); +} diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..4443e06 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,168 @@ +#include +#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 +} diff --git a/src/mps_sensor.cpp b/src/mps_sensor.cpp new file mode 100644 index 0000000..8464803 --- /dev/null +++ b/src/mps_sensor.cpp @@ -0,0 +1,335 @@ +#include "mps_sensor.h" +#include "uart_manager.h" +#include "config.h" +#include +#include + +// ===================================================== +// 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"; + } +} diff --git a/src/spi_manager.cpp b/src/spi_manager.cpp new file mode 100644 index 0000000..0cd788c --- /dev/null +++ b/src/spi_manager.cpp @@ -0,0 +1,63 @@ +#include "spi_manager.h" +#include +#include + +// ===================================================== +// 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(); +} diff --git a/src/uart_manager.cpp b/src/uart_manager.cpp new file mode 100644 index 0000000..cada566 --- /dev/null +++ b/src/uart_manager.cpp @@ -0,0 +1,199 @@ +#include "uart_manager.h" +#include +#include +#include +#include +#include + +// ===================================================== +// 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 diff --git a/test/README b/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html