feat: add M95M02-only GAS and EEPROM profiles

Expose the existing registers through Modbus and service CAN using one GAS provider only in M95M02 builds.
Include the EEPROM layout/driver, LOAD protection and calibration, firmware identity and shared CCS build workflow.
Add transport/profile and SCI8 validation tests, documentation and the checked M95M02 firmware image.
This commit is contained in:
2026-10-09 16:17:37 +03:00
parent c6c9128238
commit 5d9818291b
63 changed files with 2307 additions and 365 deletions

View File

@@ -0,0 +1,21 @@
"""Check SPI wire address widths for both production EEPROM configurations."""
from pathlib import Path
import subprocess
import os
import shutil
import tempfile
root = Path(__file__).resolve().parents[2]
protocol = root.parent / 'newProject/SETGUI/third_party/templates/c/set-protocol'
compiler = os.environ.get('CC') or shutil.which('gcc') or 'C:/msys64/mingw64/bin/gcc.exe'
os.environ['PATH'] = str(Path(compiler).resolve().parent) + os.pathsep + os.environ['PATH']
with tempfile.TemporaryDirectory() as folder:
for profile in (0, 1):
exe = Path(folder) / f'spi{profile}.exe'
subprocess.run([compiler, '-std=c99',
f'-DEEPROM_M95M02={profile}', '-Itests/eeprom_profiles/stubs',
'-ISource/Internal/Include', '-I' + str(protocol),
'tests/eeprom_profiles/spi_test.c', '-o', str(exe)],
cwd=root, check=True)
subprocess.run([str(exe)], check=True, timeout=10)
print(f'PASS SPI profile {profile}')

View File

@@ -0,0 +1,47 @@
/* Exercise the production Timer2 SPI state machine; no hardware writes. */
#include <assert.h>
#include "../../Source/Internal/spise2p.c"
static unsigned trace[300], widths[300], used;
static void transfer(unsigned long address, int write)
{
unsigned buf[2]={0x1234,0x5678}, i;
SE2P_DATA message={buf,2,address};
used=0;
if(write) spiSe2pWrite(&se2p,&message); else spiSe2pRead(&se2p,&message);
for(i=0;i<100 && !spiSe2pFree(&se2p);i++) {
SpiaRegs.SPITXBUF=0xdead;
SpiaRegs.SPIRXBUF=0; /* WIP clear for write, deterministic read data. */
SpiaRegs.SPISTS.bit.INT_FLAG=1;
SPISE2P_DRV_tick(&se2p);
if(SpiaRegs.SPITXBUF!=0xdead) {
assert(GpioDataRegs.GPADAT.bit.GPIO19==0);
trace[used]=SpiaRegs.SPITXBUF;
widths[used++]=(SpiaRegs.SPICCR.all&15)+1;
}
}
assert(spiSe2pFree(&se2p) && GpioDataRegs.GPADAT.bit.GPIO19==1);
i=0;
if(write) { assert(trace[i]==0x0600 && widths[i++]==8); }
assert(trace[i]==(write ? 0x0200 : 0x0300) && widths[i++]==8);
#if EEPROM_M95M02
assert(trace[i]==((address>>16)<<8) && widths[i++]==8);
#endif
assert(trace[i]==(address&65535) && widths[i++]==16);
assert(trace[i]==(write ? 0x1234 : 0) && widths[i++]==16);
assert(trace[i]==(write ? 0x5678 : 0) && widths[i++]==16);
if(write) { assert(trace[i]==0x0500 && widths[i++]==8); assert(trace[i++]==0); }
assert(i==used);
}
int main(void)
{
SPISE2P_DRV_init(&se2p);
transfer(0x7ffc,0); transfer(0x7ffc,1);
#if EEPROM_M95M02
assert(BALSAM167_EEPROM_FIRMWARE_BYTES==261884UL);
transfer(0xfffe,0); transfer(0x10000,0);
transfer(0x20000,1); transfer(0x3fffc,0); transfer(0x3fffc,1);
#else
assert(BALSAM167_EEPROM_FIRMWARE_BYTES==32508);
#endif
return 0;
}

View File

@@ -0,0 +1,22 @@
#ifndef EEPROM_TEST_DEVICE_H
#define EEPROM_TEST_DEVICE_H
#include <stdint.h>
#define interrupt
#define EALLOW
#define EDIS
#define CLKMULT 2
#define SYSCLKOUT 60000000UL
#define M_INT14 0x2000
static unsigned IER;
struct bits { unsigned GPIO16,GPIO17,GPIO18,GPIO19,SPISWRESET,BUFFULL_FLAG,INT_FLAG; };
struct reg { unsigned all; struct bits bit; };
static struct { struct reg GPAMUX2,GPADIR; } GpioCtrlRegs;
static struct { struct reg GPADAT; } GpioDataRegs;
static struct { void (*TINT2)(void); } PieVectTable;
static struct { unsigned InterruptCount; } CpuTimer2;
static struct { struct reg TCR; } CpuTimer2Regs;
static struct { struct reg SPICCR,SPICTL,SPISTS,SPIFFTX,SPIFFRX,SPIFFCT,SPIPRI;
unsigned SPIBRR; uint16_t SPITXBUF,SPIRXBUF; } SpiaRegs;
static void InitCpuTimers(void) {}
static void ConfigCpuTimer(void *p, unsigned long f, unsigned t) {(void)p;(void)f;(void)t;}
#endif

View File

@@ -0,0 +1,42 @@
"""Independent test oracle: does this built SCI8 file contain its manifest identity?
This is a build test, not a second production protocol codec. The 12-word vector
is deliberately constructed independently of the C implementation to detect a
stale compilation unit, incorrect byte order or a mismatched pre-build JSON.
Usage: python tests/firmware_identity/check_build.py path/to/firmware.bin
"""
from datetime import datetime
import hashlib
import json
from pathlib import Path
import struct
import sys
image = Path(sys.argv[1])
data = image.read_bytes()
meta = json.loads(image.with_suffix(image.suffix + '.manifest.json').read_text())
assert hashlib.sha256(data).hexdigest() == meta['sha256'], 'Wrong file hash'
assert len(data) == meta['size_bytes'], 'Wrong image size'
stamp = datetime.fromisoformat(meta['built_at_utc'])
build_id = meta['build_id']
words = [1, *meta['version'], stamp.year, stamp.month * 256 + stamp.day,
stamp.hour * 256 + stamp.minute, stamp.second]
words += [ord(build_id[i]) * 256 + ord(build_id[i+1]) for i in range(0, 8, 2)]
expected = struct.pack('<12H', *words)
assert data[:2] == b'\xaa\x08', 'Not SCI8'
offset = 22
found = 0
while True:
count, = struct.unpack_from('<H', data, offset)
offset += 2
if not count:
break
offset += 4 # destination address, not part of the loaded section payload
end = offset + count * 2
assert end <= len(data), 'Truncated boot block'
block = data[offset:end]
found += sum(block[i:i+24] == expected for i in range(0, len(block)-23, 2))
offset = end
assert offset == len(data), 'Trailing data'
assert found == 1, f'Expected one embedded identity, found {found}'
print(f'PASS: SCI8 contains manifest identity {meta["version"]}, {build_id}, {stamp.isoformat()}')

View File

@@ -0,0 +1,33 @@
/* Test the actual 167 binding, not just a fake protocol backend. Build against
* the generated header from the firmware build under test. No MCU registers. */
#include "firmware_identity.h"
#include "firmware_info_static.h"
#include "ump_tms28335.h"
#include <assert.h>
static unsigned logger_reads, logger_writes;
unsigned int ump_log_read(unsigned int address)
{ (void)address; ++logger_reads; return 0x554dU; }
int ump_log_write(unsigned int address, unsigned int value)
{ (void)address; (void)value; ++logger_writes; return 1; }
int main(void)
{
uint16_t words[UMP_MAX_WORDS];
ump_octet wire[UMP_RTU_CAPACITY];
unsigned error, i;
assert(ump_tms28335_prepare(3, UMP_FIRMWARE_INFO, 12, words, &error) == 12 && !error);
assert(words[0] == 1 && words[1] == FIRMWARE_VERSION_MAJOR);
assert(words[2] == FIRMWARE_VERSION_MINOR && words[3] == FIRMWARE_VERSION_PATCH);
assert(words[4] == FI_YEAR && words[5] == FI_PAIR(FI_MONTH, FI_DAY));
assert(words[8] == FI_PAIR(FI_ID_0, FI_ID_1));
assert(logger_reads == 0);
assert(ump_tms28335_rtu(1,3,UMP_FIRMWARE_INFO,12,wire,UMP_RTU_CAPACITY) == 29);
for (i=0; i<12; ++i) assert(((wire[3+i*2]<<8) | wire[4+i*2]) == words[i]);
assert(ump_crc(wire,29) == 0);
ump_tms28335_prepare(6,UMP_FIRMWARE_INFO,123,words,&error);
assert(error == 2 && logger_writes == 0);
/* Firmware reads must not steal the existing logger window. */
assert(ump_tms28335_prepare(3,UMP_BASE,16,words,&error) == 16 && !error);
assert(logger_reads == 16 && words[0] == 0x554dU);
return 0;
}

View File

@@ -0,0 +1,48 @@
"""Validate the PM35 RAM-load SCI8 image against the selected EEPROM profile."""
import argparse
from pathlib import Path
import struct
def check_image(path, eeprom):
data = path.read_bytes()
limit = {"AT25256B": 32508, "M95M02": 261884}[eeprom]
if not 24 <= len(data) <= limit:
raise ValueError(f"Image size {len(data)} outside 24..{limit}")
if struct.unpack_from("<H", data)[0] != 0x08AA:
raise ValueError("Not a SCI8 boot image")
high, low = struct.unpack_from("<HH", data, 18)
entry = (high << 16) | low
position, blocks = 22, []
while True:
if position + 2 > len(data):
raise ValueError("Missing boot terminator")
words = struct.unpack_from("<H", data, position)[0]
position += 2
if not words:
if position != len(data):
raise ValueError("Trailing bytes after boot terminator")
break
if position + 4 + words * 2 > len(data):
raise ValueError("Truncated boot block")
high, low = struct.unpack_from("<HH", data, position)
address = (high << 16) | low
end = address + words
if not 0x8000 <= address < end <= 0xD000:
raise ValueError(f"Boot destination {address:#x}..{end:#x} outside code RAM")
if any(address < old_end and old_start < end for old_start, old_end in blocks):
raise ValueError("Overlapping boot blocks")
blocks.append((address, end))
position += 4 + words * 2
if not any(start <= entry < end for start, end in blocks):
raise ValueError("Entry point not loaded by the boot table")
return len(data), len(blocks), limit - len(data)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("image", type=Path)
parser.add_argument("--eeprom", choices=("AT25256B", "M95M02"), required=True)
args = parser.parse_args()
size, blocks, free = check_image(args.image, args.eeprom)
print(f"PASS SCI8 {args.eeprom}: {size} bytes, {blocks} RAM blocks, {free} bytes free")

196
tests/gas167/gas167_test.c Normal file
View File

@@ -0,0 +1,196 @@
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "gas167.h"
#include "ump_protocol.h"
#include "ump_tms28335.h"
#include "include/balsam167_eeprom_block.h"
#define ANSWER_LEN 128U
#define MAX_SEND_LENGTH 400U
#define CNTRL_ADDR 16U
#define CMD_MODBUS_3 3U
#define CMD_MODBUS_6 6U
#define dsk_LOAD 6
#define LOBYTE(v) ((v) & 255U)
#define HIBYTE(v) (((v) >> 8) & 255U)
#define UMP_CAN_MAX_WORDS UMP_MAX_WORDS
#define ump_can_prepare ump_tms28335_prepare
typedef struct { int all; } WORDE;
#define Modbus ((WORDE *)modbus)
typedef struct {
uint16_t RS_Header[8], buffer[MAX_SEND_LENGTH];
unsigned flag_TIMEOUT_to_Send;
} RS_DATA;
int modbus[129], Desk = dsk_LOAD;
static unsigned captures, spi_reads, sent, irq_depth, legacy_reads;
static unsigned __disable_interrupts(void) {
unsigned saved = irq_depth;
++irq_depth;
return saved;
}
static void __restore_interrupts(unsigned saved) { irq_depth = saved; }
void ump_log_rs(void) { ++captures; }
unsigned int ump_log_read(unsigned int address) { ++legacy_reads; return address; }
int ump_log_write(unsigned int address, unsigned int value) { return 1; }
static unsigned get_crc_16(unsigned seed, const uint16_t *data, unsigned size) {
assert(seed == 0xFFFFU);
return ump_crc(data, size);
}
static void RS_Send(RS_DATA *rs, uint16_t *data, unsigned size) {
assert(irq_depth == 0);
sent = size;
}
static void Seeprom_read(unsigned long word, uint16_t *data, unsigned bytes) {
unsigned i;
assert(irq_depth == 0); /* The Timer2 SPI driver must retain interrupts. */
++spi_reads;
for (i = 0; i < bytes/2; ++i) data[i] = (uint16_t)(0xBEEFU+i);
}
static unsigned Load_protection_prepare(unsigned fn, unsigned a, unsigned n,
uint16_t *out, unsigned *error) {
out[0] = (uint16_t)a; out[1] = (uint16_t)n; *error = 0;
return 2;
}
struct mock_reg { uint32_t all; };
static struct {
struct mock_reg CANTA, CANAA, CANTRR, CANTRS;
} ECanaRegs;
static struct {
struct { struct mock_reg MDH, MDL; } MBOX2;
} ECanaMboxes;
static struct { unsigned long InterruptCount; } CpuTimer1;
#include "production_routes.inc"
static void request(RS_DATA *rs, unsigned fn, unsigned a, unsigned n) {
unsigned crc;
memset(rs, 0, sizeof(*rs));
rs->RS_Header[0] = CNTRL_ADDR; rs->RS_Header[1] = fn;
rs->RS_Header[2] = a >> 8; rs->RS_Header[3] = a & 255U;
rs->RS_Header[4] = n >> 8; rs->RS_Header[5] = n & 255U;
crc = ump_crc(rs->RS_Header, 6);
rs->RS_Header[6] = crc & 255U; rs->RS_Header[7] = crc >> 8;
sent = 0;
if (fn == 3) ReceiveCommandModbus3(rs);
else ReceiveCommandModbus6(rs);
assert(irq_depth == 0 && rs->flag_TIMEOUT_to_Send);
assert(ump_crc(rs->buffer, rs->buffer[1] & 128U ? 5 :
(fn == 6 ? 8 : 5+rs->buffer[2])) == 0);
}
static void can_request(unsigned fn, unsigned a, unsigned n) {
memset(&ECanaRegs, 0, sizeof(ECanaRegs));
memset(&ECanaMboxes, 0, sizeof(ECanaMboxes));
ump_can_waiting = 0; ump_can_length = 0; ump_can_index = 0;
ump_can_pending = 1; ump_can_tag = 0x0100U | fn;
ump_can_address = a; ump_can_value = n;
UMP_CAN_service();
assert(irq_depth == 0 && !ump_can_pending);
assert(ECanaRegs.CANTRS.all == 4U);
}
int main(void) {
RS_DATA rs;
unsigned i;
for (i = 0; i < 129; ++i) modbus[i] = (int)i;
modbus[128] = 0xD00D;
/* Exercise the real native handlers in both builds. */
request(&rs, 6, 1, 0xFEDC);
assert(modbus[1] == 0xFEDC && sent == 10);
request(&rs, 3, 0, 2);
assert(rs.buffer[2] == 4 && rs.buffer[5] == 0xFE && rs.buffer[6] == 0xDC);
request(&rs, 6, 0x52, 0);
assert(rs.buffer[1] == 0x86 && rs.buffer[2] == 3 && modbus[0x52] == 0x52);
request(&rs, 6, 0x7C, 9);
assert(rs.buffer[2] == 2 && modbus[0x7C] == 0x7C);
/* Existing UMP and protection routing must still work. */
request(&rs, 3, 0x1000, 2);
assert(rs.buffer[1] == 3 && rs.buffer[3] == 0x10 && legacy_reads == 2);
request(&rs, 3, 0x1300, 2);
assert(rs.buffer[1] == 3 && rs.buffer[3] == 0x13);
#if EEPROM_M95M02
{
uint16_t out[124];
unsigned error, count;
for (i = 0; i < 65536U; ++i)
assert(gas167_contains((uint16_t)i) ==
((i >= 0x5000U && i <= 0x507FU) || (i >= 0x5F00U && i <= 0x5F07U)));
assert(gas167_prepare(3, 0x5000, 124, 0, out, &error) == 124 && !error);
assert(out[1] == 0xFEDC && out[123] == 123);
count = gas167_prepare(3, 0x507F, 2, 0, out, &error);
assert(count == 2 && error == 2 && modbus[128] == 0xD00D);
assert(gas167_prepare(3, 0x5000, 0, 0, out, &error) == 2 && error == 3);
assert(gas167_prepare(3, 0x5000, 125, 0, out, &error) == 2 && error == 3);
assert(gas167_prepare(3, 0x5000, 1, 1, out, &error) == 2 && error == 3);
assert(gas167_prepare(3, 0xFFFF, 2, 0, out, &error) == 2 && error == 2);
assert(gas167_prepare(16, 0x5000, 1, 0, out, &error) == 2 && error == 1);
assert(gas167_prepare(6, 0x5080, 7, 0, out, &error) == 2 && error == 2);
assert(gas167_prepare(6, 0x5052, 60001, 0, out, &error) == 2 && error == 3);
assert(gas167_prepare(6, 0x5052, 60000, 0, out, &error) == 2 && !error);
assert(modbus[0x52] == 60000);
assert(gas167_prepare(3, 0x5000, 1, 0, 0, &error) == 0);
assert(gas167_prepare(6, 0x5000, 1, 0, out, 0) == 0 && modbus[0] == 0);
}
request(&rs, 3, 0x5000, 2);
assert(sent == 9 && rs.buffer[5] == 0xFE && rs.buffer[6] == 0xDC);
request(&rs, 3, 0x5000, 124);
assert(sent == 253 && rs.buffer[2] == 248);
request(&rs, 3, 0x5F00, 8);
assert(sent == 21 && rs.buffer[3] == 0x47 && rs.buffer[4] == 0x41);
assert(rs.buffer[5] == 0x53 && rs.buffer[6] == 0x31);
assert(rs.buffer[10] == 167 && rs.buffer[18] == 1);
request(&rs, 6, 0x5F00, 0);
assert(sent == 5 && rs.buffer[2] == 2);
request(&rs, 3, 0x5F07, 2);
assert(rs.buffer[2] == 2);
request(&rs, 6, 0x5052, 0);
assert(rs.buffer[2] == 3 && modbus[0x52] == 60000);
request(&rs, 6, 0x507D, 9);
assert(rs.buffer[2] == 2 && modbus[0x7D] == 0x7D);
request(&rs, 6, 0x507F, 0x80);
assert(sent == 8 && rs.buffer[2] == 0x50 && rs.buffer[3] == 0x7F);
assert(captures == 1 && modbus[127] == 0x80);
can_request(6, 0x5001, 0xFEDC);
assert(modbus[1] == 0xFEDC && ECanaMboxes.MBOX2.MDL.all == 0x5001FEDCUL);
assert(ECanaMboxes.MBOX2.MDH.all == 0x1F000100UL);
can_request(3, 0x5001, 2);
assert(ECanaMboxes.MBOX2.MDL.all == 0xFEDC0002UL);
can_request(3, 0x5000, 124);
assert(ump_can_length == 124 && ump_can_index == 2);
modbus[2] = 777; /* All fragments must retain the original snapshot. */
for (i = 1; i < 62; ++i) {
ECanaRegs.CANTA.all = 4;
UMP_CAN_service();
assert(ump_can_index == 2*(i+1));
if (i == 1) assert(ECanaMboxes.MBOX2.MDL.all == 0x00020003UL);
}
modbus[2] = 2;
can_request(3, 0x5000, 3);
assert(ECanaMboxes.MBOX2.MDH.all == 0x1F000103UL);
can_request(6, 0x507C, 9);
assert((ECanaMboxes.MBOX2.MDH.all & 255U) == 2 && modbus[0x7C] == 0x7C);
can_request(6, 0x507F, 0x40);
assert(captures == 2 && modbus[127] == 0x40);
can_request(6, 0x5F00, 9);
assert((ECanaMboxes.MBOX2.MDH.all & 255U) == 2);
request(&rs, 3, 0x8000, 0x8001);
#else
request(&rs, 3, 0x5000, 2);
assert(sent == 5 && rs.buffer[2] == 2);
request(&rs, 6, 0x5001, 9);
assert(rs.buffer[2] == 2 && modbus[1] == 0xFEDC);
request(&rs, 3, 0x5000, 124);
assert(sent == 5 && rs.buffer[2] == 2);
request(&rs, 3, 0x5F00, 8);
assert(rs.buffer[2] == 2);
can_request(6, 0x5001, 9);
assert((ECanaMboxes.MBOX2.MDH.all & 255U) == 2 && modbus[1] == 0xFEDC);
assert(captures == 0);
request(&rs, 3, 0x8000, 1);
#endif
assert(spi_reads == 1 && rs.buffer[3] == 0xBE && rs.buffer[4] == 0xEF);
assert(modbus[128] == 0xD00D);
printf("PASS GAS167 profile %d: production RTU/CAN routes, guards, CRC and legacy services\n", EEPROM_M95M02);
return 0;
}

51
tests/gas167/run_test.py Normal file
View File

@@ -0,0 +1,51 @@
"""Exercise the production GAS provider and extracted production RTU/CAN routes."""
import os
from pathlib import Path
import re
import shutil
import subprocess
import tempfile
root = Path(__file__).resolve().parents[2]
templates = Path(os.environ.get("TEMPLATES_ROOT", root.parent / "newProject/SETGUI/third_party/templates"))
protocol = templates / "c/set-protocol"
compiler = os.environ.get("CC") or shutil.which("gcc") or "C:/msys64/mingw64/bin/gcc.exe"
os.environ["PATH"] = str(Path(compiler).resolve().parent) + os.pathsep + os.environ["PATH"]
def function(text, name):
match = re.search(r"^(?:static )?(?:void|int) " + name + r"\(", text, re.M)
if not match:
raise ValueError(f"Production function {name} missing")
end = re.search(r"^}", text[match.start():], re.M)
if not end:
raise ValueError(f"Production function {name} incomplete")
return text[match.start():match.start() + end.end()] + "\n"
message = (root / "Source/Internal/message.c").read_text(encoding="cp1251")
ecan = (root / "Source/Internal/ecan.c").read_text(encoding="cp1251")
can_globals = ecan[ecan.index("static volatile unsigned int ump_can_pending;"):
ecan.index("static unsigned long ump_can_sent_at;") + len("static unsigned long ump_can_sent_at;")]
# On C28x unsigned int is uint16_t. Preserve that target word layout on GCC.
can_globals = can_globals.replace("static unsigned int ump_can_reply[",
"static uint16_t ump_can_reply[")
routes = can_globals + "\n" + "".join(function(message, name) for name in
("modbus_exception", "receive_ump", "ReceiveCommandModbus3", "ReceiveCommandModbus6"))
routes += function(ecan, "UMP_CAN_service")
with tempfile.TemporaryDirectory(prefix="gas167-test-") as tmp:
directory = Path(tmp)
(directory / "production_routes.inc").write_text(routes, encoding="utf-8")
for profile in (0, 1):
exe = directory / f"gas167_{profile}.exe"
subprocess.run([compiler, "-std=c99", "-Wall", "-Wextra",
"-Wno-unused-function", "-Wno-unused-parameter",
f"-DEEPROM_M95M02={profile}", "-I" + str(directory),
"-ISource/Internal/Include", "-I" + str(protocol),
"-I" + str(protocol / "include"),
"-I" + str(protocol / "ports/tms320f28335"),
"tests/gas167/gas167_test.c", "Source/Internal/gas167.c",
str(protocol / "src/ump_protocol.c"),
str(protocol / "ports/tms320f28335/ump_tms28335.c"),
"-o", str(exe)], cwd=root, check=True)
subprocess.run([str(exe)], check=True, timeout=10)

View File

@@ -0,0 +1,103 @@
#include <stdio.h>
#include <string.h>
#include "../../Source/Internal/DAC.c"
int READY = 1, Desk = 6, Mode = 5;
int modbus[128];
unsigned int test_start, test_stop, test_permission, test_tick;
unsigned int test_starts, test_last_stop_tick, test_last_start_tick, test_init_dac;
int test_bad_order;
unsigned int ump_dac_ramp, ump_dac_duration, ump_dac_go10, ump_dac_stop10, ump_dac_position;
#define CHECK(x) do { if (!(x)) { printf("FAIL line %d: %s\n", __LINE__, #x); return 1; } } while (0)
static void setup(int mode) {
memset(modbus, 0, sizeof(modbus)); Mode = mode; READY = 1;
test_start = test_stop = test_permission = test_tick = test_starts = 0;
test_bad_order = test_init_dac = 0;
Load_control_init(); Setup_DAC_time(); WAKEpowse = 0;
DAC_go = 4; DAC_stop = 16; DAC_min() = 100; DAC_max() = 3000;
}
static void tick(unsigned int n, int exchange, unsigned int volts) {
while (n--) {
if (exchange) Load_voltage_can(0x8000U | (Mode == 5 ? 305U : 8U), volts, 0, 0);
test_tick++; Load_runner();
}
}
int main(void) {
unsigned int out[8], error, count, ramp, code, stopped_at;
setup(5);
/* Boot ISR before parameters: no divide-by-zero and no GPIO start. */
Load_control_init(); Commands = 0x80; tick(10, 1, 0); CHECK(!test_starts);
Setup_DAC_time(); WAKEpowse = 0;
tick(10, 0, 0); CHECK(test_starts == 1 && test_permission);
CHECK(LOAD_CAN_TIMEOUT == 600);
/* Wrong ZPT and masked slots never confirm freshness. */
setup(5); Commands = 0x80;
Load_voltage_can(0x8000U | 8U, 200, 0, 0);
Load_voltage_can(305U, 200, 0, 0); tick(10, 0, 0);
CHECK(!test_starts && (LOAD_STATUS & 2));
Load_voltage_can(0x2000U | 303U, 0, 0, 30);
tick(1, 0, 0); CHECK(test_starts == 1);
/* Freeze current and code at timeout; unrelated exchange cannot unfreeze. */
tick(1000, 1, 30); tick(600, 0, 0);
ramp = ump_dac_ramp; code = modbus[123];
tick(2000, 0, 0); CHECK(ump_dac_ramp == ramp && (unsigned int)modbus[123] == code);
CHECK(test_permission && !test_stop && (LOAD_STATUS & 2));
Load_voltage_can(0x8000U | 8U, 200, 0, 0); tick(10, 0, 0);
CHECK(ump_dac_ramp == ramp);
tick(10, 1, 30); CHECK(ump_dac_ramp > ramp);
/* Exactly 30 V permits growth; 29 V fails strictly above 6 mA. */
setup(6); Commands = 0x80; tick(10, 1, 30);
load_count_ramp = time_dac / 6; tick(5, 1, 30);
CHECK(load_state == LOAD_RUN && modbus[0x1C] >= 60);
tick(5, 1, 29); CHECK(load_state == LOAD_RELOAD && test_stop && !test_permission);
CHECK(!test_start && ump_dac_ramp == 0 && modbus[0x1C] == 40);
CHECK(modbus[123] == DAC_min()); stopped_at = test_last_stop_tick;
tick(999, 1, 0); CHECK(test_starts == 1 && test_stop);
tick(1, 1, 0); CHECK(test_starts == 2 && test_last_start_tick-stopped_at == 1000);
CHECK(!test_stop && test_permission && !test_bad_order);
/* Five total attempts, then a persistent fault; no sixth start. */
while (load_state != LOAD_FAULT) {
load_count_ramp = time_dac / 6; tick(5, 1, 0);
if (load_state == LOAD_RELOAD) tick(1000, 1, 0);
}
CHECK(test_starts == 5 && LOAD_ATTEMPTS == 5 && (LOAD_STATUS & 1));
tick(3000, 1, 200); CHECK(test_starts == 5 && test_stop && !test_permission);
Commands = 0xC0; tick(10, 1, 200); CHECK(load_state == LOAD_FAULT);
Commands = 0; tick(1000, 1, 200); CHECK(!(LOAD_STATUS & 1));
Commands = 0x80; test_starts = 0; tick(10, 1, 200); CHECK(LOAD_ATTEMPTS == 1);
/* Manual reset dominates START; releasing it alone cannot start again. */
Commands = 0xC0; tick(1, 1, 200); CHECK(test_stop && !test_permission);
Commands = 0x80; tick(1500, 1, 200); CHECK(test_starts == 1);
/* Versioned simulation writes are bounded, read-only fields reject writes. */
count = Load_protection_prepare(3, 0x1300, 8, out, &error);
CHECK(count == 8 && !error && out[0] == 0x5550 && out[1] == 1);
Load_protection_prepare(6, 0x1303, 0, out, &error); CHECK(error == 3);
Load_protection_prepare(6, 0x1304, 0, out, &error); CHECK(error == 2);
Load_protection_prepare(6, 0x1302, 4, out, &error); CHECK(error == 3);
setup(5); Commands = 0x80; tick(10, 1, 200);
Load_protection_prepare(6, 0x1303, 20, out, &error); CHECK(!error);
Load_protection_prepare(6, 0x1302, 1, out, &error); CHECK(!error);
tick(30, 1, 200); CHECK(LOAD_STATUS & 2);
ramp = ump_dac_ramp; tick(100, 1, 200); CHECK(ump_dac_ramp == ramp);
Load_protection_prepare(6, 0x1302, 0, out, &error);
tick(10, 1, 200); CHECK(!(LOAD_STATUS & 2) && ump_dac_ramp > ramp);
Load_protection_prepare(6, 0x1302, 2, out, &error);
load_count_ramp = time_dac / 6; tick(5, 1, 200); CHECK(load_state == LOAD_RELOAD);
/* Calibration must not replace the forced 4 mA output during reload. */
Commands |= 0x200; DAC_cal() = 4095; tick(5, 1, 200);
CHECK(modbus[123] == DAC_min() && modbus[0x1C] == 40);
/* A calibration code preceding START must be replaced by 4 mA first. */
setup(5); Commands = 0x200; DAC_cal() = 4095;
tick(10, 1, 200); CHECK(modbus[123] == 4095 && !load_zero_sent);
Commands = 0x280; tick(1, 1, 200); CHECK(!test_starts);
tick(4, 1, 200); CHECK(!test_starts && modbus[123] == DAC_min());
tick(1, 1, 200); CHECK(test_starts == 1);
/* Timeout while STOP is held cannot initiate the pending retry. */
setup(5); Commands = 0x80; tick(10, 1, 0);
load_count_ramp = time_dac / 6; tick(5, 1, 0);
CHECK(load_state == LOAD_RELOAD);
tick(1500, 0, 0); CHECK(test_starts == 1 && test_stop);
tick(1, 1, 0); CHECK(test_starts == 2 && !test_stop);
Load_control_init(); CHECK(!load_simulation);
puts("PASS: production DAC control, timeout, CAN slots, STOP ordering, five attempts, simulation");
return 0;
}

View File

@@ -0,0 +1,16 @@
"""Compile the production DAC.c against GPIO/IRQ mocks; no hardware writes."""
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
root = Path(__file__).resolve().parents[2]
compiler = os.environ.get('CC') or shutil.which('gcc') or 'C:/msys64/mingw64/bin/gcc.exe'
os.environ['PATH'] = str(Path(compiler).resolve().parent) + os.pathsep + os.environ['PATH']
with tempfile.TemporaryDirectory(prefix='load-control-') as tmp:
exe = Path(tmp) / 'test.exe'
subprocess.run([compiler, '-Wall', '-Wextra', '-std=c99',
'-Itests/load_control/stubs', '-ISource/Internal/Include',
'tests/load_control/load_control_test.c', '-o', str(exe)], cwd=root, check=True)
subprocess.run([str(exe)], check=True, timeout=10)

View File

@@ -0,0 +1,9 @@
#ifndef TEST_DEVICE_H
#define TEST_DEVICE_H
struct bits { unsigned int GPIO59, GPIO60, GPIO63; };
struct reg { struct bits bit; };
static struct { struct reg GPBSET, GPBCLEAR; } GpioDataRegs;
static unsigned int __disable_interrupts(void) { return 0; }
static void __restore_interrupts(unsigned int saved) { (void)saved; }
static void DSP28x_usDelay(unsigned long n) { (void)n; }
#endif

View File

@@ -0,0 +1 @@
/* No IRQ registers needed in the host test. */

View File

@@ -0,0 +1 @@
/* No serial dependencies in the host test. */

View File

@@ -0,0 +1 @@
/* No filter dependencies in the host test. */

View File

@@ -0,0 +1,4 @@
#define READY_FREQ 1000
#define DAC_FREQ 200
#define LOAD_TIME 30
extern int READY;

View File

@@ -0,0 +1 @@
extern int modbus[128];

View File

@@ -0,0 +1,17 @@
#define dsk_LOAD 6
#define adr_LOA1 5
#define adr_LOA2 6
#define Commands modbus[127]
#define cUMPstart (Commands & 0x80)
#define cUMPreset (Commands & 0x40)
extern unsigned int test_init_dac;
#define cInitDac test_init_dac
#define cCalibrDac (Commands & 0x200)
#define DAC_go modbus[0x50]
#define DAC_stop modbus[0x51]
#define LOAD_CAN_TIMEOUT modbus[0x52]
#define LOAD_STATUS modbus[0x7C]
#define LOAD_ATTEMPTS modbus[0x7D]
#define DAC_min() modbus[0x79]
#define DAC_max() modbus[0x78]
#define DAC_cal() modbus[0x7A]

View File

@@ -0,0 +1,14 @@
extern int Desk, Mode;
extern unsigned int test_start, test_stop, test_permission, test_tick;
extern unsigned int test_starts, test_last_stop_tick, test_last_start_tick;
extern int test_bad_order;
static void clear_START_DPTB(void) { test_start = 0; }
static void set_START_DPTB(void) {
if (test_stop || (test_starts && test_tick-test_last_stop_tick < 1000)) test_bad_order = 1;
test_start = 1; test_last_start_tick = test_tick; test_starts++;
}
static void set_STOP_DPTB(void) { test_stop = 1; test_last_stop_tick = test_tick; }
static void clear_STOP_DPTB(void) { test_stop = 0; }
static void clear_RES_OUT_2(void) { test_permission = 0; }
static void set_RES_OUT_2(void) { if (!test_start || test_stop) test_bad_order = 1; test_permission = 1; }
static void toggle_RES_OUT_1(void) {}

View File

@@ -0,0 +1 @@
/* Delay provided by the device stub. */

View File

@@ -16,7 +16,7 @@ int main(void)
ump_time = ump_sequence = 0xA5A5A5A5UL;
ump_bank = ump_archive_bank = 0xA5A5;
ump_archive_count = ump_archive_next = ump_generation = 0xA5A5;
ump_source = ump_post = ump_post_dropped = 0xA5A5;
ump_source = ump_post = 0xA5A5;
ump_can_header = ump_can_count = 0xA5A5;
for (i = 0; i < 3; i++) ump_can_data[i] = 0xA5A5;
for (i = 0; i < 2; i++) ump_pm67_voltage[i] = 0xA5A5;
@@ -34,7 +34,7 @@ int main(void)
for (i = 12; i <= 15; i++) CHECK(ump_log_read(UMP_LOG_BASE+i) == 0);
CHECK(ump_bank == 0 && ump_archive_bank == 1);
CHECK(ump_cursor == 0 && ump_next == 0 && ump_archive_next == 0);
CHECK(ump_divider == 0 && ump_sequence == 0 && ump_post_dropped == 0);
CHECK(ump_divider == 0 && ump_sequence == 0 && ump_post == 0);
CHECK(ump_can_header == 0 && ump_can_count == 0);
for (i = 0; i < 3; i++) CHECK(ump_can_data[i] == 0);
for (i = 0; i < 2; i++) CHECK(ump_pm67_voltage[i] == 0);

View File

@@ -44,16 +44,22 @@ __declspec(dllexport) int run_tests(void)
/* Короткая CAN-команда должна остаться отдельным событием. */
modbus[127] = 0x40;
ump_log_can(0x807F, 0x40, 0, 0);
CHECK(ump_post == 1000);
CHECK(ump_word(0, ump_count-1, 2) == 0x102);
CHECK(ump_post == 0); /* A command alone is not an output transition. */
GpioDataRegs.GPBDAT.bit.GPIO58 = 1;
ump_log_io(); /* STOP may be sampled before RES_OUT_2 is cleared. */
CHECK(ump_post == 0);
GpioDataRegs.GPADAT.bit.GPIO3 = 1;
ump_log_io();
CHECK(ump_post == UMP_POST_POINTS);
CHECK(ump_word(0, ump_count-1, 2) == 0x103);
CHECK(ump_word(0, ump_count-1, 18) == 0x807F);
for (i=0; i<999; i++) ump_log_tick();
for (i=0; i<UMP_POST_POINTS-1; i++) ump_log_tick();
CHECK(ump_generation == 0);
ump_log_tick();
CHECK(ump_generation == 1);
CHECK(ump_archive_count == UMP_LOG_CAPACITY);
CHECK(ump_word(1, UMP_LOG_CAPACITY/2-1, 2) == 0x102);
CHECK(ump_word(1, UMP_LOG_CAPACITY-1, 2) == 0x205);
CHECK(ump_word(1, UMP_LOG_CAPACITY-UMP_POST_POINTS-1, 2) == 0x103);
CHECK(ump_word(1, UMP_LOG_CAPACITY-1, 2) == 5);
CHECK(ump_count == 0 && ump_state == 1);
CHECK(ump_word(1, UMP_LOG_CAPACITY-1, 40) == 123);
CHECK(ump_word(1, UMP_LOG_CAPACITY-1, 41) == 789);
@@ -80,12 +86,56 @@ __declspec(dllexport) int run_tests(void)
CHECK(sequence == ump_sequence);
CHECK(ump_log_write(UMP_LOG_BASE, 1));
CHECK(ump_word(1, UMP_LOG_CAPACITY-1, 0) == archived);
/* Новый пуск и спад start без reset также фиксируют отключение. */
/* Repeated reset/STOP pulses beyond the post interval preserve archive. */
for (i=0; i<3; i++) {
modbus[127] = 0; GpioDataRegs.GPBDAT.bit.GPIO58 = 0; ump_log_io();
modbus[127] = 0x40; ump_log_can(0x807F, 0x40, 0, 0);
GpioDataRegs.GPBDAT.bit.GPIO58 = 1; ump_log_io();
CHECK(ump_post == 0);
{ unsigned int tick; for (tick=0; tick<1100; tick++) ump_log_tick(); }
CHECK(ump_generation == generation);
CHECK(ump_word(1, UMP_LOG_CAPACITY-1, 0) == archived);
}
/* Falling start alone does not freeze; a new real cycle does. */
modbus[127] = 0x80; ump_log_rs();
GpioDataRegs.GPBDAT.bit.GPIO58 = 0;
GpioDataRegs.GPADAT.bit.GPIO3 = 0; ump_log_io();
modbus[127] = 0; ump_log_rs();
CHECK(ump_post == 1000);
for (i=0; i<1000; i++) ump_log_tick();
CHECK(ump_post == 0);
GpioDataRegs.GPBDAT.bit.GPIO58 = 1;
GpioDataRegs.GPADAT.bit.GPIO3 = 1; ump_log_io();
CHECK(ump_post == UMP_POST_POINTS);
for (i=0; i<UMP_POST_POINTS; i++) ump_log_tick();
CHECK(ump_generation == generation+1);
/* A RES_OUT_2 fall without STOP is not a shutdown trigger. */
generation = ump_generation;
GpioDataRegs.GPBDAT.bit.GPIO58 = 0;
GpioDataRegs.GPADAT.bit.GPIO3 = 0; ump_log_io();
GpioDataRegs.GPADAT.bit.GPIO3 = 1; ump_log_io();
CHECK(ump_post == 0);
/* Starting the recorder with stopped outputs must not invent an edge. */
CHECK(ump_log_write(UMP_LOG_BASE, 0));
GpioDataRegs.GPBDAT.bit.GPIO58 = 1;
CHECK(ump_log_write(UMP_LOG_BASE, 1));
CHECK(ump_post == 0 && ump_generation == generation);
/* At a slow sample period time alone cannot finish the point budget.
* CAN and RS records count, and repeated edges do not restart it. */
CHECK(ump_log_write(UMP_LOG_BASE, 0));
CHECK(ump_log_write(UMP_LOG_BASE+1, 1000));
CHECK(ump_log_write(UMP_LOG_BASE, 1));
GpioDataRegs.GPADAT.bit.GPIO3 = 0; ump_log_io();
GpioDataRegs.GPADAT.bit.GPIO3 = 1; ump_log_io();
CHECK(ump_post == UMP_POST_POINTS);
for (i=0; i<1000; i++) ump_log_tick();
CHECK(ump_post == UMP_POST_POINTS-1 && ump_generation == generation);
GpioDataRegs.GPADAT.bit.GPIO3 = 0; ump_log_io();
GpioDataRegs.GPADAT.bit.GPIO3 = 1; ump_log_io();
CHECK(ump_post == UMP_POST_POINTS-3);
for (i=0; i<UMP_POST_POINTS-4; i++) ump_log_can(0x807F, 0, 0, 0);
CHECK(ump_post == 1 && ump_generation == generation);
ump_log_rs();
CHECK(ump_post == 0 && ump_generation == generation+1);
CHECK(ump_archive_count == UMP_POST_POINTS+3);
/* CAN читает те же регистры и формирует цельный снимок из 124 слов.
* Запись по старым технологическим адресам через сервис запрещена. */
length = ump_can_prepare(3, UMP_LOG_BASE, 16, reply, &error);

View File

@@ -18,11 +18,18 @@ if not compiler:
raise SystemExit('Clang or GCC is required in PATH')
link_flags = [] if 'gcc' in Path(compiler).name else ['-nostdlib', '-fuse-ld=lld', '-Wl,/noentry,/nodefaultlib']
with tempfile.TemporaryDirectory(prefix='ump-recorder-') as directory:
# Exercise the production identity binding too, using a build-local header.
subprocess.run([
os.sys.executable, str(root / 'UKSSTMS320F28335/prepare_identity.py'),
'--project', str(root / 'UKSSTMS320F28335'), '--templates', str(templates),
'--output', directory,
], check=True)
library = Path(directory) / 'recorder_test.dll'
subprocess.run([
compiler, '-shared', *link_flags, '-Wno-unknown-pragmas', '-Wno-int-to-pointer-cast',
'-Itests/ump_logger/stubs', '-ISource/Internal/Include',
'-I' + str(protocol), '-I' + str(protocol / 'ports/tms320f28335'),
'-I' + str(templates / 'c/firmware-info/include'), '-I' + directory,
'tests/ump_logger/recorder_test.c', 'Source/Internal/shared_ump.c', '-o', str(library),
], cwd=root, check=True)
# Отдельный процесс освобождает DLL до удаления временной папки Windows.

View File

@@ -1,8 +1,8 @@
"""Exercise the release gate without TI tools or hardware."""
"""Exercise the current board-specific SCI8 gate without TI tools or hardware."""
from pathlib import Path
import shutil
import struct
import subprocess
import sys
import tempfile
import unittest
@@ -12,34 +12,35 @@ LIMIT = (0x3FFF - (128 + 1)) * 2
def image(size):
header = struct.pack('<H', 0x08AA) + bytes(16) + struct.pack('<HH', 0, 0x8000)
words = (size - 30) // 2
return header + struct.pack('<HHH', words, 0, 0x8000) + bytes(words * 2) + bytes(2)
blocks = max(1, (size-24+131075)//131076)
remaining = (size-24-blocks*6)//2
result = bytearray(header)
for _ in range(blocks):
words = min(65535, remaining)
result += struct.pack('<HHH', words, 0, 0x8000) + bytes(words*2)
remaining -= words
return bytes(result)+bytes(2)
class FirmwareGateTest(unittest.TestCase):
def check_image(self, data, success):
def check_image(self, data, success, profile="AT25256B"):
with tempfile.TemporaryDirectory() as directory:
source = Path(directory) / 'candidate.pending'
output = Path(directory) / 'firmware.bin'
source = Path(directory) / 'candidate.bin'
source.write_bytes(data)
result = subprocess.run([
shutil.which('powershell') or 'pwsh', '-NoProfile', '-ExecutionPolicy', 'Bypass',
'-File', str(ROOT / 'check_firmware.ps1'), '-Path', str(source),
'-PublishPath', str(output),
sys.executable, str(ROOT / 'tests/gas167/check_image.py'),
str(source), '--eeprom', profile,
], capture_output=True)
self.assertEqual(result.returncode == 0, success, result.stderr.decode(errors='replace'))
self.assertEqual(output.exists(), success)
if success:
self.assertEqual(output.read_bytes(), data)
self.assertFalse(source.exists())
self.assertEqual(result.returncode == 0, success,
result.stderr.decode(errors='replace'))
self.assertEqual(source.read_bytes(), data)
return result.stdout + result.stderr
def test_exact_boundary_is_accepted(self):
self.check_image(image(LIMIT), True)
def test_34a95bc5_size_is_rejected(self):
diagnostic = self.check_image(image(32518), False)
self.assertIn(b'overlaps saved parameters by 10 bytes', diagnostic)
self.check_image(image(32518), False)
def test_missing_boot_terminator_is_rejected(self):
self.check_image(image(100)[:-2], False)
@@ -47,6 +48,15 @@ class FirmwareGateTest(unittest.TestCase):
def test_partial_block_is_rejected(self):
self.check_image(image(100)[:-10], False)
def test_m95_capacity_does_not_expand_code_ram(self):
diagnostic = self.check_image(image(261884), False, "M95M02")
self.assertIn(b'outside code RAM', diagnostic)
self.check_image(image(261886), False, "M95M02")
def test_m95_image_rejected_for_small_part(self):
self.check_image(image(32734), True, "M95M02")
self.check_image(image(32734), False, "AT25256B")
def test_wrong_format_is_rejected(self):
self.check_image(bytes(100), False)