Объединить протокол терминала TMS2812 для Python и Android

This commit is contained in:
2026-10-01 15:41:43 +03:00
parent e3e30fdad7
commit 3457657979
14 changed files with 729 additions and 9 deletions

View File

@@ -0,0 +1,263 @@
package ru.setcorp.setflash.core
import ru.setcorp.setprotocol.NativeSetProtocol
/** Shared templates facade for BALZAM TMS320F2812; framing and CRC use C99. */
object Tms2812Protocol {
const val CMD_LOAD = 51
const val CMD_UPLOAD = 52
const val CMD_RUN = 53
const val CMD_XFLASH = 54
const val CMD_TFLASH = 55
const val CMD_PEEK = 56
const val CMD_POKE = 57
const val CMD_INITLOAD = 58
const val CMD_INIT = 59
const val CMD_EXTEND = 60
const val CMD_VECTOR = 61
const val CMD_IMPULSE = 62
const val CMD_STD = 65
const val CMD_TEST_ALL = 66
// BALZAM CHIEF bios.c returns 0x0055 at ADR_FOR_SPECIAL without reading
// the peripheral bus. Address 0 goes through read_bus_special(), whose
// failure suppresses the reply entirely, so it is unsuitable for PING.
const val PING_MEMORY_ADDRESS = 0x00000100L
// extendbios() operation selectors from BALZAM_167/Src/mybalzam_7/bios.c.
const val EXTEND_FLASH_REMOTE_EEPROM = 6
const val EXTEND_READ_REMOTE_EEPROM = 7
const val EXTEND_RESET_TO_INTERNAL_FLASH = 9
const val EXTEND_FLASH_SPARTAN6 = 10
const val EXTEND_VERIFY_REMOTE_EEPROM = 17
const val ACK_SIZE = 6
const val PEEK_RESPONSE_SIZE = 10
const val STD_RESPONSE_SIZE = 112
const val UPLOAD_RESPONSE_OVERHEAD = 8
const val EXTEND_RESPONSE_SIZE = 18
data class StandardResponse(
val digital: ByteArray,
val analog: List<Int>,
)
data class ExtendResponse(
val processedWords: Long,
val errorCode: Long,
val repeatErrors: Long,
)
fun crc16(data: ByteArray, initial: Int = 0xFFFF): Int {
require(initial == 0xFFFF) {
"Произвольное начальное значение CRC не входит в протокол ПМ67"
}
requireNative()
return NativeSetProtocol.nativeTms2812Crc16(data)
}
fun initLoad(address: Int, ramAddress: Long, length: Int): ByteArray = fixed(
address,
CMD_INITLOAD,
le32(ramAddress) + le32(length.toLong()),
12,
)
/**
* LOAD is address + command, followed by raw data and CRC. RS485.c consumes
* the first two bytes as a normal header, switches RS_FlagBegin off for
* CMD_LOAD, and stores the remaining Length + 2 bytes in RAM.
*/
fun loadData(address: Int, image: ByteArray): ByteArray {
return fixed(address, CMD_LOAD, image, image.size + 4)
}
fun tFlash(address: Int, ramAddress: Long, flashAddress: Long, length: Int): ByteArray = fixed(
address,
CMD_TFLASH,
le32(ramAddress) + le32(flashAddress) + le32(length.toLong()),
16,
)
fun upload(address: Int, memoryAddress: Long, length: Int): ByteArray {
require(address in 0..255) { "Адрес контроллера должен быть 0…255" }
require(memoryAddress in 0..0xFFFF_FFFFL) { "Адрес слова вне диапазона uint32" }
require(length > 0) { "Размер блока должен быть положительным" }
requireNative()
return requireNotNull(
NativeSetProtocol.nativeTms2812BuildUpload(
address, memoryAddress, length.toLong(),
),
) { "SETProtocol отклонил CMD_UPLOAD ПМ67" }
}
/**
* bios.c sends UPLOAD as address, command, packed bytes, CRC16 and four
* trailing bytes. Unlike ordinary replies its CRC immediately follows data.
*/
fun validateUploadResponse(packet: ByteArray, address: Int, length: Int): Boolean {
if (length <= 0 || address !in 0..255) return false
requireNative()
return NativeSetProtocol.nativeTms2812ValidateUpload(packet, address, length)
}
fun uploadData(packet: ByteArray, address: Int, length: Int): ByteArray {
requireNative()
return requireNotNull(
NativeSetProtocol.nativeTms2812DecodeUpload(packet, address, length),
) { "Поврежден ответ CMD_UPLOAD" }
}
fun peek(address: Int, memoryAddress: Long): ByteArray =
memory(address, CMD_PEEK, memoryAddress, 0)
fun ping(address: Int): ByteArray = memory(address, 0, 0, 0)
fun poke(address: Int, memoryAddress: Long, value: Int): ByteArray =
memory(address, CMD_POKE, memoryAddress, value)
private fun memory(address: Int, command: Int, memoryAddress: Long, value: Int): ByteArray {
require(address in 0..255 && memoryAddress in 0..0xFFFF_FFFFL && value in 0..65535) {
"Адрес или значение TMS2812 вне диапазона"
}
requireNative()
return requireNotNull(NativeSetProtocol.nativeTms2812BuildMemory(address, command, memoryAddress, value))
}
fun testAll(address: Int, duration: Int, pause: Int, digits: ByteArray): ByteArray {
require(address in 0..255 && duration in 0..65535 && pause in 0..65535 && digits.size == 12)
requireNative()
return requireNotNull(NativeSetProtocol.nativeTms2812BuildTest(address, duration, pause, digits))
}
fun extend(
address: Int,
firstAddress: Long,
secondAddress: Long,
length: Long,
code1: Int,
code2: Int,
): ByteArray = fixed(
address,
CMD_EXTEND,
le32(firstAddress) + le32(secondAddress) + le32(length) + byteArrayOf(code1.toByte(), code2.toByte()),
18,
)
fun parseExtendResponse(packet: ByteArray, address: Int): ExtendResponse {
require(packet.size == EXTEND_RESPONSE_SIZE && validateResponse(packet, address, CMD_EXTEND)) {
"Поврежден ответ CMD_EXTEND"
}
return ExtendResponse(
processedWords = le32Value(packet, 2),
errorCode = le32Value(packet, 6),
repeatErrors = le32Value(packet, 10),
)
}
fun standard(address: Int, analog: List<Int>, flags: Set<Int>): ByteArray {
require(analog.size == 4) { "CMD_STD должен содержать четыре аналоговых значения" }
val payload = ByteArray(14)
analog.forEachIndexed { index, value ->
require(value in Short.MIN_VALUE..Short.MAX_VALUE) { "Аналог ${index + 1} вне диапазона Int16" }
payload[index * 2] = value.toByte()
payload[index * 2 + 1] = (value ushr 8).toByte()
}
flags.forEach { oneBased ->
require(oneBased in 1..48) { "Номер флага должен быть от 1 до 48" }
val bit = oneBased - 1
payload[8 + bit / 8] = (payload[8 + bit / 8].toInt() or (1 shl (bit % 8))).toByte()
}
return fixed(address, CMD_STD, payload, 18)
}
fun validateResponse(packet: ByteArray, address: Int, command: Int): Boolean {
requireNative()
return NativeSetProtocol.nativeTms2812ValidateResponse(packet, address, command, packet.size, false)
}
/**
* BALZAM replies can omit final reserved bytes after CRC (two in ordinary
* replies, four in UPLOAD data replies).
* Call after the receive settle interval so a delayed tail cannot overlap
* the next request. Never pad missing data or CRC bytes. UPLOAD uses its
* own validator because its CRC precedes four trailing bytes, not two.
*/
fun normalizeResponseTail(
packet: ByteArray,
address: Int,
command: Int,
fullSize: Int,
): ByteArray? {
val uploadData = command == CMD_UPLOAD && fullSize >= UPLOAD_RESPONSE_OVERHEAD
val reservedBytes = if (uploadData) 4 else 2
if (fullSize < ACK_SIZE || packet.size !in (fullSize - reservedBytes)..fullSize) return null
val normalized = packet.copyOf(fullSize)
val valid = if (uploadData) {
validateUploadResponse(normalized, address, fullSize - UPLOAD_RESPONSE_OVERHEAD)
} else {
requireNative()
NativeSetProtocol.nativeTms2812ValidateResponse(packet, address, command, fullSize, true)
}
return normalized.takeIf { valid }
}
fun peekValue(packet: ByteArray, address: Int): Int {
require(packet.size == PEEK_RESPONSE_SIZE && validateResponse(packet, address, CMD_PEEK)) {
"Поврежден ответ CMD_PEEK"
}
return (packet[2].toInt() and 0xFF) or ((packet[3].toInt() and 0xFF) shl 8)
}
fun parseStandard(packet: ByteArray, address: Int): StandardResponse {
require(packet.size == STD_RESPONSE_SIZE && validateResponse(packet, address, CMD_STD)) {
"Поврежден ответ CMD_STD"
}
val digital = packet.copyOfRange(2, 40)
val analog = (0 until 34).map { index ->
val offset = 40 + index * 2
(((packet[offset].toInt() and 0xFF) or
((packet[offset + 1].toInt() and 0xFF) shl 8)).toShort()).toInt()
}
return StandardResponse(digital, analog)
}
fun commandName(command: Int): String = when (command) {
CMD_LOAD -> "CMD_LOAD"
CMD_UPLOAD -> "CMD_UPLOAD"
CMD_RUN -> "CMD_RUN"
CMD_XFLASH -> "CMD_XFLASH"
CMD_TFLASH -> "CMD_TFLASH"
CMD_PEEK -> "CMD_PEEK"
CMD_POKE -> "CMD_POKE"
CMD_INITLOAD -> "CMD_INITLOAD"
CMD_INIT -> "CMD_INIT"
CMD_EXTEND -> "CMD_EXTEND"
CMD_VECTOR -> "CMD_VECTOR"
CMD_IMPULSE -> "CMD_IMPULSE"
CMD_STD -> "CMD_STD"
CMD_TEST_ALL -> "CMD_TEST_ALL"
else -> "CMD_$command"
}
private fun fixed(address: Int, command: Int, payload: ByteArray, size: Int): ByteArray {
require(address in 0..255) { "Адрес контроллера должен быть 0…255" }
require(payload.size + 4 == size) { "Неверная длина пакета" }
requireNative()
return requireNotNull(NativeSetProtocol.nativeTms2812BuildFixed(address, command, payload))
}
private fun le32(value: Long): ByteArray = ByteArray(4) { index ->
(value ushr (index * 8)).toByte()
}
private fun le32Value(data: ByteArray, offset: Int): Long =
(data[offset].toLong() and 0xFF) or
((data[offset + 1].toLong() and 0xFF) shl 8) or
((data[offset + 2].toLong() and 0xFF) shl 16) or
((data[offset + 3].toLong() and 0xFF) shl 24)
private fun requireNative() {
check(NativeSetProtocol.available) { "Нативное ядро SETProtocol недоступно" }
}
}

View File

@@ -53,6 +53,11 @@ object NativeSetProtocol {
external fun nativePeriph28335ProjectName(projectIndex: Int): String?
external fun nativePeriph28335CommandName(projectIndex: Int, commandIndex: Int): String?
external fun nativeTms2812Crc16(input: ByteArray): Int
external fun nativeTms2812BuildFixed(controller: Int, command: Int, payload: ByteArray): ByteArray?
external fun nativeTms2812BuildMemory(controller: Int, command: Int, address: Long, value: Int): ByteArray?
external fun nativeTms2812BuildTest(controller: Int, duration: Int, pause: Int, digits: ByteArray): ByteArray?
external fun nativeTms2812ValidateResponse(input: ByteArray, controller: Int, command: Int,
fullSize: Int, allowShortTail: Boolean): Boolean
external fun nativeTms2812BuildUpload(controller: Int, wordAddress: Long, byteCount: Long): ByteArray?
external fun nativeTms2812ExpectedUploadSize(byteCount: Long): Int
external fun nativeTms2812ValidateUpload(input: ByteArray, controller: Int, byteCount: Int): Boolean

View File

@@ -10,6 +10,79 @@
#include "periph28335.h"
#include "tms2812.h"
static jbyteArray tms_result(JNIEnv *env, const uint8_t *data, size_t size)
{
if (size == 0U || size > INT32_MAX) return NULL;
jbyteArray result = (*env)->NewByteArray(env, (jsize)size);
if (result != NULL) (*env)->SetByteArrayRegion(env, result, 0, (jsize)size, (const jbyte *)data);
return result;
}
JNIEXPORT jbyteArray JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTms2812BuildFixed(
JNIEnv *env, jobject self, jint controller, jint command, jbyteArray input)
{
(void)self;
if (input == NULL || controller < 0 || controller > 255 || command < 0 || command > 255) return NULL;
jsize size = (*env)->GetArrayLength(env, input);
if (size > INT32_MAX - 4) return NULL;
jbyte *data = (*env)->GetByteArrayElements(env, input, NULL);
if (data == NULL) return NULL;
uint8_t *output = (uint8_t *)malloc((size_t)size + 4U);
if (output == NULL) { (*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT); return NULL; }
size_t written = tms2812_build_fixed_request((uint8_t)controller, (uint8_t)command,
(const uint8_t *)data, (size_t)size, output, (size_t)size + 4U);
(*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT);
jbyteArray result = tms_result(env, output, written);
free(output);
return result;
}
JNIEXPORT jbyteArray JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTms2812BuildMemory(
JNIEnv *env, jobject self, jint controller, jint command, jlong address, jint value)
{
(void)self;
uint8_t output[12];
if (controller < 0 || controller > 255 || command < 0 || command > 255 ||
address < 0 || (uint64_t)address > UINT32_MAX || value < 0 || value > 65535) return NULL;
size_t written = tms2812_build_memory_request((uint8_t)controller, (uint8_t)command,
(uint32_t)address, (uint16_t)value, output, sizeof output);
return tms_result(env, output, written);
}
JNIEXPORT jbyteArray JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTms2812BuildTest(
JNIEnv *env, jobject self, jint controller, jint duration, jint pause, jbyteArray input)
{
(void)self;
uint8_t output[20];
if (input == NULL || controller < 0 || controller > 255 || duration < 0 ||
duration > 65535 || pause < 0 || pause > 65535 || (*env)->GetArrayLength(env, input) != 12) return NULL;
jbyte *data = (*env)->GetByteArrayElements(env, input, NULL);
if (data == NULL) return NULL;
size_t written = tms2812_build_test_request((uint8_t)controller, (uint16_t)duration,
(uint16_t)pause, (const uint8_t *)data, 12U, output, sizeof output);
(*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT);
return tms_result(env, output, written);
}
JNIEXPORT jboolean JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTms2812ValidateResponse(
JNIEnv *env, jobject self, jbyteArray input, jint controller, jint command,
jint full_size, jboolean allow_short_tail)
{
(void)self;
if (input == NULL || controller < 0 || controller > 255 || command < 0 || command > 255 || full_size < 6) return JNI_FALSE;
jsize size = (*env)->GetArrayLength(env, input);
jbyte *data = (*env)->GetByteArrayElements(env, input, NULL);
if (data == NULL) return JNI_FALSE;
int status = tms2812_validate_response((const uint8_t *)data, (size_t)size,
(uint8_t)controller, (uint8_t)command, (size_t)full_size, allow_short_tail != JNI_FALSE);
(*env)->ReleaseByteArrayElements(env, input, data, JNI_ABORT);
return status == TMS2812_OK ? JNI_TRUE : JNI_FALSE;
}
JNIEXPORT jint JNICALL
Java_ru_setcorp_setprotocol_NativeSetProtocol_nativeTms2812Crc16(
JNIEnv *env, jobject self, jbyteArray input)

View File

@@ -0,0 +1,26 @@
package ru.setcorp.setflash.core
import org.junit.Assert.*
import org.junit.Test
class TmsTerminalSharedTest {
@Test fun testAllUsesSharedNativeFraming() {
val digits = byteArrayOf(0x81.toByte(), 2, 4, 8, 9, 0x20, 0x10, 0x20, 0x40, 0x80.toByte(), 3, 4)
val packet = Tms2812Protocol.testAll(7, 25, 18, digits)
assertEquals(20, packet.size)
assertArrayEquals(byteArrayOf(7, 66, 25, 0, 18, 0) + digits, packet.copyOf(18))
assertEquals(0, Tms2812Protocol.crc16(packet))
}
@Test fun pokeKeepsWordAddressAndZeroPadding() {
val packet = Tms2812Protocol.poke(10, 0x12345678L, 0xABCD)
assertArrayEquals(byteArrayOf(10, 57, 0x78, 0x56, 0x34, 0x12,
0xCD.toByte(), 0xAB.toByte(), 0, 0), packet.copyOf(10))
assertEquals(0, Tms2812Protocol.crc16(packet))
}
@Test(expected = IllegalArgumentException::class)
fun invalidAddressIsNotTruncated() {
Tms2812Protocol.peek(10, 0x1_0000_0000L)
}
}