From 196d0830981d6027d915ec89eb64bb98f9833acc Mon Sep 17 00:00:00 2001 From: Andrey Date: Thu, 1 Oct 2026 18:19:37 +0300 Subject: [PATCH 1/3] =?UTF-8?q?feat(tms2812):=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D1=8C=20=D0=BE=D0=B1=D1=89=D0=B8=D0=B9=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D1=82=D0=BE=D0=BA=D0=BE=D0=BB=20=D1=82=D0=B5=D1=80=D0=BC?= =?UTF-8?q?=D0=B8=D0=BD=D0=B0=D0=BB=D0=B0=20=D0=B8=20=D0=BF=D1=80=D0=B8?= =?UTF-8?q?=D0=B2=D1=8F=D0=B7=D0=BA=D0=B8=20Python=20=D0=B8=20Android?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- c/set-protocol/TMS2812.md | 37 +++ c/set-protocol/include/pcan_abi.h | 8 + c/set-protocol/include/tms2812.h | 20 +- .../setcorp/setflash/core/Tms2812Protocol.kt | 263 ++++++++++++++++++ .../setcorp/setprotocol/NativeSetProtocol.kt | 5 + .../ports/android/setprotocol_jni.c | 73 +++++ .../setflash/core/TmsTerminalSharedTest.kt | 26 ++ c/set-protocol/src/pcan_abi.c | 24 ++ c/set-protocol/src/tms2812.c | 55 ++++ c/set-protocol/tests/test_tms2812.c | 32 +++ python/protocan/tms_terminal.py | 134 +++++++++ python/tests/test_tms_terminal_shared.py | 43 +++ 12 files changed, 719 insertions(+), 1 deletion(-) create mode 100644 c/set-protocol/TMS2812.md create mode 100644 c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/Tms2812Protocol.kt create mode 100644 c/set-protocol/ports/android/tests/ru/setcorp/setflash/core/TmsTerminalSharedTest.kt create mode 100644 python/protocan/tms_terminal.py create mode 100644 python/tests/test_tms_terminal_shared.py diff --git a/c/set-protocol/TMS2812.md b/c/set-protocol/TMS2812.md new file mode 100644 index 0000000..e4c6e31 --- /dev/null +++ b/c/set-protocol/TMS2812.md @@ -0,0 +1,37 @@ +# Legacy TMS2812 / BALZAM protocol + +`src/tms2812.c` is the shared wire implementation for the host terminal, +memory access, firmware framing and CRC validation. The wire format is the +legacy controller protocol, **not SETProtocol v2**. The shared library name +does not select or change the wire format. + +- `tms2812_build_memory_request`: PEEK (56), POKE (57), host operation 0 + for PING (PEEK of service word address `0x00000100`). +- `tms2812_build_test_request`: TEST_ALL (66), two uint16 timings and + twelve control bytes. +- `tms2812_build_fixed_request`: address, command, payload, CRC16/MODBUS. +- `tms2812_validate_response`: checks controller, command, length and CRC; + may accept omission of the two reserved bytes after CRC. The transport + must wait for a receive gap before sending the next command. +- UPLOAD (52) retains its separate validator: four reserved trailing bytes. + +Bindings: + +- Python: `python/protocan/tms_terminal.py` calls the exported `pcan_abi.h` + functions through ctypes. `set_devices.tms_terminal` keeps telemetry + layouts/parsing; it uses the C framing for CMD_STD. +- Kotlin: `ports/android/kotlin/ru/setcorp/setflash/core/Tms2812Protocol.kt` + calls the same C functions through `setprotocol_jni.c` and + `NativeSetProtocol`. Consumers include the templates Kotlin source tree; + they must not keep a second Tms2812Protocol class in their app source tree. + +Consumers own UART I/O, timeout/settle timers, sequential write/readback +transactions, GUI field names and JSON display profiles. They do not own +copies of request builders or response CRC checks. + +Rebuild the native library after updating templates. There is no fallback +to a separate local encoder if the terminal ABI is missing. + +Tests: `tests/test_tms2812.c`, `python/tests/test_tms_terminal_shared.py`, +and the Android `TmsTerminalSharedTest`. All use the same Android/captured +PING and PEEK vectors and TEST_ALL field layout. diff --git a/c/set-protocol/include/pcan_abi.h b/c/set-protocol/include/pcan_abi.h index 7413a84..1417723 100644 --- a/c/set-protocol/include/pcan_abi.h +++ b/c/set-protocol/include/pcan_abi.h @@ -102,6 +102,14 @@ PCAN_ABI_API const char *pcan_abi_periph28335_command_name( size_t project_index, size_t command_index); /* PM67/TMS320F2812 legacy memory upload. */ +PCAN_ABI_API size_t pcan_abi_tms2812_build_fixed(uint8_t controller, uint8_t command, + const uint8_t *payload, size_t payload_size, uint8_t *output, size_t output_size); +PCAN_ABI_API size_t pcan_abi_tms2812_build_memory(uint8_t controller, uint8_t command, + uint32_t word_address, uint16_t value, uint8_t *output, size_t output_size); +PCAN_ABI_API size_t pcan_abi_tms2812_build_test(uint8_t controller, uint16_t duration, + uint16_t pause, const uint8_t *digits, size_t digit_count, uint8_t *output, size_t output_size); +PCAN_ABI_API int pcan_abi_tms2812_validate_response(const uint8_t *data, size_t size, + uint8_t controller, uint8_t command, size_t full_size, int allow_short_tail); PCAN_ABI_API uint16_t pcan_abi_tms2812_crc16( const uint8_t *data, size_t size); PCAN_ABI_API size_t pcan_abi_tms2812_build_upload( diff --git a/c/set-protocol/include/tms2812.h b/c/set-protocol/include/tms2812.h index d40ee56..a2a25df 100644 --- a/c/set-protocol/include/tms2812.h +++ b/c/set-protocol/include/tms2812.h @@ -1,6 +1,6 @@ /** * @file tms2812.h - * @brief Shared PM67/TMS320F2812 legacy upload protocol. + * @brief Shared PM67/TMS320F2812 legacy terminal and memory protocol. */ #ifndef TMS2812_H #define TMS2812_H @@ -13,6 +13,10 @@ extern "C" { #endif #define TMS2812_CMD_UPLOAD 52U +#define TMS2812_CMD_PEEK 56U +#define TMS2812_CMD_POKE 57U +#define TMS2812_CMD_TEST_ALL 66U +#define TMS2812_PING_ADDRESS 0x00000100UL #define TMS2812_UPLOAD_REQUEST_SIZE 12U #define TMS2812_UPLOAD_RESPONSE_OVERHEAD 8U @@ -28,6 +32,20 @@ typedef enum { uint16_t tms2812_crc16(const uint8_t *data, size_t size); +size_t tms2812_build_fixed_request(uint8_t controller, uint8_t command, + const uint8_t *payload, size_t payload_size, uint8_t *output, size_t output_size); +/* command=0 is the host PING operation; on wire it is PEEK at 0x100. */ +size_t tms2812_build_memory_request(uint8_t controller, uint8_t command, + uint32_t word_address, uint16_t value, uint8_t *output, size_t output_size); +size_t tms2812_build_test_request(uint8_t controller, uint16_t duration, + uint16_t pause, const uint8_t *digits, size_t digit_count, + uint8_t *output, size_t output_size); +/* Ordinary BALZAM responses reserve two bytes after CRC. If allow_short_tail + * is true only those bytes may be omitted; caller must observe the UART gap. + * UPLOAD responses use tms2812_validate_upload_response instead. */ +int tms2812_validate_response(const uint8_t *data, size_t size, + uint8_t controller, uint8_t command, size_t full_size, int allow_short_tail); + size_t tms2812_build_upload_request( uint8_t controller, uint32_t word_address, uint32_t byte_count, uint8_t *output, size_t output_size); diff --git a/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/Tms2812Protocol.kt b/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/Tms2812Protocol.kt new file mode 100644 index 0000000..58cbfd6 --- /dev/null +++ b/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/Tms2812Protocol.kt @@ -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, + ) + + 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, flags: Set): 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 недоступно" } + } +} diff --git a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/NativeSetProtocol.kt b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/NativeSetProtocol.kt index cc08f61..f910841 100644 --- a/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/NativeSetProtocol.kt +++ b/c/set-protocol/ports/android/kotlin/ru/setcorp/setprotocol/NativeSetProtocol.kt @@ -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 diff --git a/c/set-protocol/ports/android/setprotocol_jni.c b/c/set-protocol/ports/android/setprotocol_jni.c index c96b2ce..c786d93 100644 --- a/c/set-protocol/ports/android/setprotocol_jni.c +++ b/c/set-protocol/ports/android/setprotocol_jni.c @@ -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) diff --git a/c/set-protocol/ports/android/tests/ru/setcorp/setflash/core/TmsTerminalSharedTest.kt b/c/set-protocol/ports/android/tests/ru/setcorp/setflash/core/TmsTerminalSharedTest.kt new file mode 100644 index 0000000..6fa2097 --- /dev/null +++ b/c/set-protocol/ports/android/tests/ru/setcorp/setflash/core/TmsTerminalSharedTest.kt @@ -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) + } +} diff --git a/c/set-protocol/src/pcan_abi.c b/c/set-protocol/src/pcan_abi.c index 800b840..70581c6 100644 --- a/c/set-protocol/src/pcan_abi.c +++ b/c/set-protocol/src/pcan_abi.c @@ -168,6 +168,30 @@ uint16_t pcan_abi_tms2812_crc16(const uint8_t *data, size_t size) return tms2812_crc16(data, size); } +size_t pcan_abi_tms2812_build_fixed(uint8_t controller, uint8_t command, + const uint8_t *payload, size_t payload_size, uint8_t *output, size_t output_size) +{ + return tms2812_build_fixed_request(controller, command, payload, payload_size, output, output_size); +} + +size_t pcan_abi_tms2812_build_memory(uint8_t controller, uint8_t command, + uint32_t word_address, uint16_t value, uint8_t *output, size_t output_size) +{ + return tms2812_build_memory_request(controller, command, word_address, value, output, output_size); +} + +size_t pcan_abi_tms2812_build_test(uint8_t controller, uint16_t duration, + uint16_t pause, const uint8_t *digits, size_t digit_count, uint8_t *output, size_t output_size) +{ + return tms2812_build_test_request(controller, duration, pause, digits, digit_count, output, output_size); +} + +int pcan_abi_tms2812_validate_response(const uint8_t *data, size_t size, + uint8_t controller, uint8_t command, size_t full_size, int allow_short_tail) +{ + return tms2812_validate_response(data, size, controller, command, full_size, allow_short_tail); +} + size_t pcan_abi_tms2812_build_upload( uint8_t controller, uint32_t word_address, uint32_t byte_count, uint8_t *output, size_t output_size) diff --git a/c/set-protocol/src/tms2812.c b/c/set-protocol/src/tms2812.c index 333923f..184633f 100644 --- a/c/set-protocol/src/tms2812.c +++ b/c/set-protocol/src/tms2812.c @@ -18,6 +18,61 @@ uint16_t tms2812_crc16(const uint8_t *data, size_t size) return set_crc16_modbus(data, size); } +size_t tms2812_build_fixed_request(uint8_t controller, uint8_t command, + const uint8_t *payload, size_t payload_size, uint8_t *output, size_t output_size) +{ + uint16_t crc; + if (output == NULL || (payload == NULL && payload_size != 0U) || + payload_size > SIZE_MAX - 4U || output_size < payload_size + 4U) return 0U; + output[0] = controller; + output[1] = command; + if (payload_size != 0U) memcpy(output + 2U, payload, payload_size); + crc = tms2812_crc16(output, payload_size + 2U); + output[payload_size + 2U] = (uint8_t)crc; + output[payload_size + 3U] = (uint8_t)(crc >> 8); + return payload_size + 4U; +} + +size_t tms2812_build_memory_request(uint8_t controller, uint8_t command, + uint32_t word_address, uint16_t value, uint8_t *output, size_t output_size) +{ + uint8_t payload[8]; + if (command == 0U) { command = TMS2812_CMD_PEEK; word_address = TMS2812_PING_ADDRESS; } + if (command != TMS2812_CMD_PEEK && command != TMS2812_CMD_POKE) return 0U; + put_le32(payload, word_address); + put_le32(payload + 4U, value); + return tms2812_build_fixed_request(controller, command, payload, + command == TMS2812_CMD_PEEK ? 4U : 8U, output, output_size); +} + +size_t tms2812_build_test_request(uint8_t controller, uint16_t duration, + uint16_t pause, const uint8_t *digits, size_t digit_count, + uint8_t *output, size_t output_size) +{ + uint8_t payload[16]; + if (digits == NULL || digit_count != 12U) return 0U; + payload[0] = (uint8_t)duration; payload[1] = (uint8_t)(duration >> 8); + payload[2] = (uint8_t)pause; payload[3] = (uint8_t)(pause >> 8); + memcpy(payload + 4U, digits, 12U); + return tms2812_build_fixed_request(controller, TMS2812_CMD_TEST_ALL, + payload, sizeof payload, output, output_size); +} + +int tms2812_validate_response(const uint8_t *data, size_t size, + uint8_t controller, uint8_t command, size_t full_size, int allow_short_tail) +{ + size_t offset; + uint16_t crc; + if (data == NULL) return TMS2812_ERROR_ARGUMENT; + if (full_size < 6U) return TMS2812_ERROR_ARGUMENT; + if (size > full_size || size < full_size - (allow_short_tail ? 2U : 0U)) + return TMS2812_ERROR_LENGTH; + if (data[0] != controller || data[1] != command) return TMS2812_ERROR_HEADER; + offset = full_size - 4U; + crc = (uint16_t)(data[offset] | ((uint16_t)data[offset + 1U] << 8)); + return crc == tms2812_crc16(data, offset) ? TMS2812_OK : TMS2812_ERROR_CRC; +} + size_t tms2812_build_upload_request( uint8_t controller, uint32_t word_address, uint32_t byte_count, uint8_t *output, size_t output_size) diff --git a/c/set-protocol/tests/test_tms2812.c b/c/set-protocol/tests/test_tms2812.c index 50be35b..677a614 100644 --- a/c/set-protocol/tests/test_tms2812.c +++ b/c/set-protocol/tests/test_tms2812.c @@ -3,8 +3,40 @@ #include "tms2812.h" +static int test_terminal(void) +{ + uint8_t output[20], digits[12] = {0x81, 2, 4, 8, 9, 0x20, 0x10, 0x20, 0x40, 0x80, 3, 4}; + const uint8_t ping[] = {0x0a, 0x38, 0, 1, 0, 0, 0xf0, 0xb4}; + uint8_t reply[] = {0x0a, 0x38, 0x65, 0xa5, 0, 0, 0xaf, 0x9b, 0, 0}; + const uint8_t test_prefix[] = {7, 66, 25, 0, 18, 0}; + size_t size; + if (tms2812_build_memory_request(10, 0, 0, 0, output, sizeof output) != 8U || + memcmp(output, ping, sizeof ping) != 0) return 10; + if (tms2812_build_memory_request(10, TMS2812_CMD_POKE, 0x12345678UL, 0xABCD, + output, sizeof output) != 12U || output[2] != 0x78 || output[5] != 0x12 || + output[6] != 0xCD || output[7] != 0xAB || output[8] != 0 || output[9] != 0 || + tms2812_crc16(output, 12U) != 0U) return 11; + if (tms2812_build_memory_request(10, 99, 0, 0, output, sizeof output) != 0U || + tms2812_build_memory_request(10, 56, 0, 0, output, 7) != 0U) return 12; + if (tms2812_build_test_request(7, 25, 18, digits, 12, output, sizeof output) != 20U || + memcmp(output, test_prefix, sizeof test_prefix) || memcmp(output + 6, digits, 12) || + tms2812_crc16(output, 20) != 0U) return 13; + if (tms2812_build_test_request(7, 25, 18, digits, 11, output, sizeof output) != 0U || + tms2812_build_fixed_request(7, 65, NULL, 1, output, sizeof output) != 0U) return 14; + for (size = 8; size <= 10; ++size) + if (tms2812_validate_response(reply, size, 10, 56, 10, 1) != TMS2812_OK) return 15; + if (tms2812_validate_response(reply, 7, 10, 56, 10, 1) != TMS2812_ERROR_LENGTH || + tms2812_validate_response(reply, 8, 10, 56, 10, 0) != TMS2812_ERROR_LENGTH || + tms2812_validate_response(reply, 10, 11, 56, 10, 1) != TMS2812_ERROR_HEADER) return 16; + reply[2] ^= 1; + if (tms2812_validate_response(reply, 10, 10, 56, 10, 1) != TMS2812_ERROR_CRC) return 17; + return 0; +} + int main(void) { + int terminal_status = test_terminal(); + if (terminal_status != 0) return terminal_status; uint8_t request[TMS2812_UPLOAD_REQUEST_SIZE]; static const uint8_t prefix[] = { 0x05U, 0x34U, 0x78U, 0x56U, 0x34U, 0x12U, diff --git a/python/protocan/tms_terminal.py b/python/protocan/tms_terminal.py new file mode 100644 index 0000000..f4a4973 --- /dev/null +++ b/python/protocan/tms_terminal.py @@ -0,0 +1,134 @@ +"""Thin host binding for legacy TMS2812 requests and ordinary responses. + +The wire implementation is c/set-protocol/src/tms2812.c, also used by JNI. +This is not the SETProtocol v2 wire format. +""" +import ctypes as ct + +from .native import get_native_protocol, NativeProtocolUnavailable +from .tms2812 import crc16_modbus + +PEEK = 56 +POKE = 57 +PING_ADDRESS = 0x100 + + +def _api(name, arguments, result=ct.c_size_t): + try: + function = getattr(get_native_protocol().lib, "pcan_abi_tms2812_" + name) + except AttributeError as error: + raise NativeProtocolUnavailable("Пересоберите templates: отсутствует TMS2812 terminal API") from error + function.argtypes = arguments + function.restype = result + return function + + +def _range(value, maximum, name): + if not isinstance(value, int) or not 0 <= value <= maximum: + raise ValueError(f"{name} вне диапазона 0…{maximum}") + + +def build_fixed_request(controller, command, payload): + _range(controller, 255, "Адрес контроллера") + _range(command, 255, "Команда") + payload = bytes(payload) + output = ct.create_string_buffer(len(payload) + 4) + function = _api("build_fixed", [ct.c_uint8, ct.c_uint8, ct.c_void_p, ct.c_size_t, ct.c_void_p, ct.c_size_t]) + size = function(controller, command, payload, len(payload), output, len(output)) + if not size: + raise ValueError("Не удалось сформировать команду TMS2812") + return output.raw[:size] + + +def _memory(controller, command, address, value): + _range(controller, 255, "Адрес контроллера") + _range(address, 0xFFFFFFFF, "Адрес слова") + _range(value, 0xFFFF, "Значение") + output = ct.create_string_buffer(12) + function = _api("build_memory", [ct.c_uint8, ct.c_uint8, ct.c_uint32, ct.c_uint16, ct.c_void_p, ct.c_size_t]) + size = function(controller, command, address, value, output, len(output)) + if not size: + raise ValueError("Не удалось сформировать команду памяти TMS2812") + return output.raw[:size] + + +def request(controller, address, value=None): + return _memory(controller, PEEK if value is None else POKE, address, 0 if value is None else value) + + +def ping(controller): + return _memory(controller, 0, 0, 0) + + +def build_test_request(address, duration, pause, digits): + _range(address, 255, "Адрес контроллера") + _range(duration, 65535, "Длительность") + _range(pause, 65535, "Пауза") + digits = bytes(digits) + if len(digits) != 12: + raise ValueError("CMD_TEST_ALL: требуется 12 байт управления") + output = ct.create_string_buffer(20) + function = _api("build_test", [ct.c_uint8, ct.c_uint16, ct.c_uint16, ct.c_void_p, ct.c_size_t, ct.c_void_p, ct.c_size_t]) + size = function(address, duration, pause, digits, len(digits), output, len(output)) + if not size: + raise ValueError("Не удалось сформировать CMD_TEST_ALL") + return output.raw[:size] + + +def validate_response(raw, controller, command, full_size, allow_short_tail=False): + _range(controller, 255, "Адрес контроллера") + _range(command, 255, "Команда") + if full_size < 6: + raise ValueError("Неверная длина ответа TMS2812") + function = _api("validate_response", [ct.c_void_p, ct.c_size_t, ct.c_uint8, ct.c_uint8, ct.c_size_t, ct.c_int], ct.c_int) + status = function(bytes(raw), len(raw), controller, command, full_size, bool(allow_short_tail)) + errors = {-1: "Неверный формат ответа TMS2812", -3: "Неверная длина ответа TMS2812", + -4: "CRC ответа TMS2812 не совпадает", -5: "Неверный адрес или команда ответа TMS2812"} + if status: + raise ValueError(errors.get(status, f"Ошибка TMS2812: {status}")) + + +def decode_reply(raw, controller, command): + if command not in (PEEK, POKE): + raise ValueError("Ожидается CMD_PEEK или CMD_POKE") + validate_response(raw, controller, command, 10 if command == PEEK else 6, True) + return int.from_bytes(raw[2:4], "little") if command == PEEK else None + + +def memory_response(raw, controller, command): + """Find a PEEK/POKE response in a UART buffer, as Android feedLegacy does. + + Returns None until data plus CRC are present. A short valid result still + needs the transport's settle timer before the next request. Ignore noise, + an optional preliminary ACK, and bytes after the complete response. + """ + _range(controller, 255, "Адрес контроллера") + if command not in (PEEK, POKE): + raise ValueError("Ожидается CMD_PEEK или CMD_POKE") + header = bytes((controller, command)) + start = bytes(raw).find(header) + if start < 0: + return None + data = bytes(raw[start:]) + size = 10 if command == PEEK else 6 + if size > 6: + for ack_size in (6, 4): + if data[ack_size:ack_size + 2] != header: + continue + try: + validate_response(data[:ack_size], controller, command, 6, True) + except ValueError: + continue + data = data[ack_size:] + break + if len(data) >= size: + packet = data[:size] + validate_response(packet, controller, command, size) + return packet + if len(data) >= size - 2: + try: + validate_response(data, controller, command, size, True) + except ValueError: + return None + return data + return None diff --git a/python/tests/test_tms_terminal_shared.py b/python/tests/test_tms_terminal_shared.py new file mode 100644 index 0000000..18ef1d5 --- /dev/null +++ b/python/tests/test_tms_terminal_shared.py @@ -0,0 +1,43 @@ +"""Wire vectors shared with Android and the native TMS2812 host test.""" +import pytest +from protocan import tms_terminal as tms + + +def test_ping_and_poke_vectors(): + assert tms.ping(10).hex() == "0a3800010000f0b4" + raw = tms.request(10, 0x12345678, 0xABCD) + assert raw[:10].hex() == "0a3978563412cdab0000" + assert tms.crc16_modbus(raw) == 0 + + +def test_test_all_vector(): + digits = bytes.fromhex("810204080920102040800304") + raw = tms.build_test_request(7, 25, 18, digits) + assert raw[:18].hex() == "074219001200810204080920102040800304" + assert len(raw) == 20 and tms.crc16_modbus(raw) == 0 + + +def test_response_accepts_only_missing_reserved_bytes(): + raw = bytes.fromhex("0a3865a50000af9b0000") + for size in (8, 9, 10): + assert tms.decode_reply(raw[:size], 10, 56) == 0xA565 + for invalid in (raw[:7], raw + b"\0", raw[:2] + b"\0" + raw[3:]): + with pytest.raises(ValueError): + tms.decode_reply(invalid, 10, 56) + with pytest.raises(ValueError): + tms.decode_reply(raw, 11, 56) + + +@pytest.mark.parametrize("args", [(256, 0, None), (1, -1, None), (1, 2**32, None), (1, 0, -1), (1, 0, 65536)]) +def test_rejects_out_of_range_before_native_conversion(args): + with pytest.raises(ValueError): + tms.request(*args) + + +def test_uart_response_ignores_noise_and_preliminary_ack(): + response = bytes.fromhex("0a3865a50000af9b0000") + ack = tms.build_fixed_request(10, 56, b"") + for prefix in (b"", ack, ack + b"\0\0"): + assert tms.memory_response(b"noise" + prefix + response, 10, 56) == response + assert tms.memory_response(response, 11, 56) is None + assert tms.memory_response(response[:7], 10, 56) is None From 6df5674996fd20c6613309a8e02cbaa9c23ee77b Mon Sep 17 00:00:00 2001 From: Andrey Date: Thu, 1 Oct 2026 18:19:38 +0300 Subject: [PATCH 2/3] =?UTF-8?q?feat(hdl):=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D1=8C=20=D0=B4=D0=B2=D1=83=D1=85=D1=82=D0=B0=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D0=B2=D1=8B=D0=B9=20=D0=B1=D1=83=D1=84=D0=B5=D1=80=20=D0=BB?= =?UTF-8?q?=D0=BE=D0=B3=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=BE=D0=B3=D0=BE=20?= =?UTF-8?q?=D0=B0=D0=BD=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hdl/logic-analyzer/README.md | 23 +++++++++++++++++ hdl/logic-analyzer/rtl/la_capture_buffer.v | 29 ++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 hdl/logic-analyzer/README.md create mode 100644 hdl/logic-analyzer/rtl/la_capture_buffer.v diff --git a/hdl/logic-analyzer/README.md b/hdl/logic-analyzer/README.md new file mode 100644 index 0000000..39c97f4 --- /dev/null +++ b/hdl/logic-analyzer/README.md @@ -0,0 +1,23 @@ +# Logic-analyzer capture memory + +`rtl/la_capture_buffer.v` is a portable Verilog-2001 true dual-port capture +buffer. It deliberately contains no Altera/Intel primitive, board pin, UART, +or trigger logic. Quartus infers Cyclone IV M9K memory from the registered +read and synchronous write ports. + +Parameters: + +| Parameter | Meaning | Default | +|---|---|---:| +| `SAMPLE_WIDTH` | Number of simultaneously captured digital inputs | 16 | +| `ADDR_WIDTH` | Buffer depth as `2**ADDR_WIDTH` | 12 (4096 samples) | + +The write and read clocks may be different. The caller owns clock-domain +crossing for addresses/status and must keep a read address stable until the +registered `rd_data` is valid on the following `rd_clk` edge. + +Example Quartus source assignment: + +```tcl +set_global_assignment -name VERILOG_FILE path/to/templates/hdl/logic-analyzer/rtl/la_capture_buffer.v +``` diff --git a/hdl/logic-analyzer/rtl/la_capture_buffer.v b/hdl/logic-analyzer/rtl/la_capture_buffer.v new file mode 100644 index 0000000..4665139 --- /dev/null +++ b/hdl/logic-analyzer/rtl/la_capture_buffer.v @@ -0,0 +1,29 @@ +// Portable dual-clock capture RAM for FPGA logic analyzers. +// The module intentionally contains no vendor primitives: Quartus infers M9K +// blocks on Cyclone IV, while other tools may infer their native block RAM. +module la_capture_buffer #( + parameter SAMPLE_WIDTH = 16, + parameter ADDR_WIDTH = 12 +) ( + input wire wr_clk, + input wire wr_en, + input wire [ADDR_WIDTH-1:0] wr_addr, + input wire [SAMPLE_WIDTH-1:0] wr_data, + + input wire rd_clk, + input wire [ADDR_WIDTH-1:0] rd_addr, + output reg [SAMPLE_WIDTH-1:0] rd_data +); + localparam DEPTH = (1 << ADDR_WIDTH); + + reg [SAMPLE_WIDTH-1:0] memory [0:DEPTH-1]; + + always @(posedge wr_clk) begin + if (wr_en) + memory[wr_addr] <= wr_data; + end + + // Registered read is supported directly by Cyclone IV M9K blocks. + always @(posedge rd_clk) + rd_data <= memory[rd_addr]; +endmodule From 46ecbe32109e3cfc29e8dc2b464065030fb1ca8c Mon Sep 17 00:00:00 2001 From: Andrey Date: Thu, 1 Oct 2026 18:19:38 +0300 Subject: [PATCH 3/3] =?UTF-8?q?docs:=20=D0=BE=D0=BF=D0=B8=D1=88=D0=B8=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=83=20=D1=81=20=D1=81=D0=B0?= =?UTF-8?q?=D0=B1=D0=BC=D0=BE=D0=B4=D1=83=D0=BB=D1=8F=D0=BC=D0=B8=20=D0=B8?= =?UTF-8?q?=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D1=8C=20=D1=81=D0=B1=D0=BE?= =?UTF-8?q?=D1=80=D0=BA=D1=83=20Doxygen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Doxyfile | 27 ++++++ NEW_PROJECT.md | 185 ++++++++++++++++++++++++++++++++++++++++++ README.md | 9 ++ doc/build-doxygen.ps1 | 28 +++++++ doc/index.html | 17 +++- doc/submodules.dox | 96 ++++++++++++++++++++++ 6 files changed, 361 insertions(+), 1 deletion(-) create mode 100644 Doxyfile create mode 100644 doc/build-doxygen.ps1 create mode 100644 doc/submodules.dox diff --git a/Doxyfile b/Doxyfile new file mode 100644 index 0000000..eb06c33 --- /dev/null +++ b/Doxyfile @@ -0,0 +1,27 @@ +PROJECT_NAME = "templates" +PROJECT_BRIEF = "Переносимые библиотеки и правила интеграции" +OUTPUT_DIRECTORY = .codex-build/doxygen +CREATE_SUBDIRS = NO +OUTPUT_LANGUAGE = Russian +INPUT = README.md NEW_PROJECT.md CONTRIBUTING.md doc/submodules.dox c python +FILE_PATTERNS = *.h *.c *.dox *.md +RECURSIVE = YES +USE_MDFILE_AS_MAINPAGE = README.md +MARKDOWN_SUPPORT = YES +AUTOLINK_SUPPORT = YES +EXTRACT_ALL = NO +EXTRACT_STATIC = NO +SOURCE_BROWSER = YES +INLINE_SOURCES = NO +STRIP_FROM_PATH = . +GENERATE_HTML = YES +HTML_OUTPUT = html +GENERATE_TREEVIEW = YES +DISABLE_INDEX = NO +GENERATE_LATEX = NO +QUIET = YES +WARNINGS = YES +WARN_IF_UNDOCUMENTED = NO +WARN_IF_DOC_ERROR = YES +WARN_AS_ERROR = FAIL_ON_WARNINGS +HAVE_DOT = NO diff --git a/NEW_PROJECT.md b/NEW_PROJECT.md index e6137d3..c3aeb12 100644 --- a/NEW_PROJECT.md +++ b/NEW_PROJECT.md @@ -120,3 +120,188 @@ USB gs_usb и конкретной разводки адаптера; перен Так новый проект не является копией старого: он использует общие ядра одной версии, а аппаратные отличия видны в одном небольшом каталоге `port/`. + +## Слияние и обновление `templates` как Git submodule + +### Что именно хранит основной проект + +Основной проект не хранит содержимое `lib/templates` в своей истории. Вместо +этого он хранит **gitlink** — ссылку на один конкретный commit репозитория +`templates`. Поэтому при работе есть два независимых уровня истории: + +1. изменения библиотек сливаются и публикуются в репозитории `templates`; +2. основной проект отдельным коммитом переводит gitlink на проверенную ревизию + `templates`. + +Обычный `git merge` в основном проекте не переносит исходники между ветками +`templates` и не создаёт merge-коммит внутри сабмодуля. Он может только выбрать +одну из уже существующих ссылок на commit. Если ссылки разошлись, сначала нужно +получить общий commit в самом `templates`, а затем зафиксировать его в основном +проекте. + +Текущую зафиксированную ревизию удобно смотреть из корня основного проекта: + +```bash +git submodule status lib/templates +git diff --submodule=log +``` + +Символ `-` перед SHA в `git submodule status` означает, что сабмодуль ещё не +инициализирован; `+` — что рабочая копия сабмодуля находится не на том commit, +который записан в основном проекте; `U` — конфликт gitlink. + +### Клонирование проекта и переключение его веток + +Новый клон лучше сразу создавать вместе с сабмодулями: + +```bash +git clone --recurse-submodules +cd <основной-проект> +``` + +Если проект уже клонирован: + +```bash +git submodule update --init --recursive +``` + +После `git switch`, `git checkout`, `git pull` или завершения слияния основной +проект может начать ссылаться на другую ревизию `templates`. Рабочее дерево +сабмодуля следует явно привести к записанному состоянию: + +```bash +git submodule update --init --recursive +``` + +Это нормальный и воспроизводимый режим: внутри `lib/templates` обычно будет +detached HEAD, потому что проект фиксирует commit, а не ветку. Не следует делать +там `git pull`, пока не выбрана рабочая ветка и не понятно, какую ревизию должен +получить основной проект. + +### Обычное обновление проекта на новую версию `templates` + +Сначала изменение должно быть проверено, закоммичено, отправлено и слито в +целевую ветку самого репозитория `templates`. После этого из корня основного +проекта выполняется: + +```bash +git -C lib/templates fetch origin +git -C lib/templates switch --detach origin/master +git add lib/templates +git diff --cached --submodule=log +git commit -m "build(templates): обновить общие библиотеки" +``` + +Перед коммитом нужно собрать основной проект и прогнать его тесты: успешные +тесты в `templates` подтверждают работу библиотеки, но не проверяют конкретный +порт, настройки платы и интеграцию приложения. + +Коммит основного проекта содержит только переход со старого SHA сабмодуля на +новый. Случайные незакоммиченные файлы внутри `lib/templates` в него не входят. +Проверить оба уровня перед фиксацией можно так: + +```bash +git status +git -C lib/templates status +git diff --submodule=log +``` + +Не нужно обновлять сабмодуль на последний `master` автоматически при каждой +сборке. Зафиксированный SHA нужен именно для того, чтобы одна и та же версия +проекта всегда собиралась с одной и той же версией библиотек. + +### Если библиотеку правят из рабочего дерева основного проекта + +Нельзя коммитить изменение, оставаясь на detached HEAD: такой commit легко +потерять при следующем `git submodule update`. Сначала внутри сабмодуля создаётся +обычная ветка: + +```bash +git -C lib/templates fetch origin +git -C lib/templates switch -c feat/<имя> origin/master +``` + +Затем изменения коммитятся и отправляются именно в репозиторий `templates`: + +```bash +git -C lib/templates add <файлы> +git -C lib/templates commit -m "feat(<библиотека>): <описание>" +git -C lib/templates push -u origin feat/<имя> +``` + +После проверки ветка сливается в `master` репозитория `templates`. Только после +публикации итогового commit основной проект обновляет свой gitlink по процедуре +из предыдущего раздела. Не следует отправлять в общий основной проект ссылку на +commit сабмодуля, которого ещё нет на сервере: остальные разработчики и CI не +смогут его получить. + +### Что происходит при слиянии веток основного проекта + +Возможны три ситуации: + +1. Gitlink изменён только в одной ветке. Git обычно принимает эту ревизию + автоматически. После merge нужно выполнить `git submodule update` и тесты. +2. Обе ветки указывают на разные commits, но один commit `templates` является + предком другого. Следует выбрать более новый проверенный commit, выполнить + `git add lib/templates` и продолжить merge. +3. Обе ветки указывают на расходящиеся commits `templates`. Это настоящий + конфликт истории сабмодуля: выбирать один SHA наугад нельзя, потому что так + потеряются изменения второй ветки. + +При конфликте сначала смотрят, какие ссылки пришли с обеих сторон: + +```bash +git ls-files -u lib/templates +git -C lib/templates fetch origin +git -C lib/templates log --oneline --graph --decorate --all +``` + +Затем в отдельной ветке **репозитория `templates`** объединяют оба commit, +устраняют конфликты исходников, запускают тесты и публикуют результат: + +```bash +git -C lib/templates switch -c merge/<имя> +git -C lib/templates merge +# исправить конфликты и запустить тесты templates +git -C lib/templates add <исправленные-файлы> +git -C lib/templates commit +git -C lib/templates push -u origin merge/<имя> +``` + +Если общий merge-коммит уже существует в `origin/master`, создавать ещё один не +нужно: достаточно выбрать существующий SHA. После получения итогового commit +конфликт завершается на уровне основного проекта: + +```bash +git -C lib/templates switch --detach <итоговый-sha> +git add lib/templates +git diff --cached --submodule=log +git merge --continue +git submodule update --init --recursive +``` + +`git add lib/templates` здесь отмечает разрешённым именно gitlink. Не нужно +удалять каталог, копировать библиотеку поверх него или разрешать конфликт как +обычный текстовый файл. + +### Что должно попасть в merge request основного проекта + +Перед отправкой результата следует проверить: + +1. итоговый commit `templates` доступен в удалённом репозитории; +2. `git -C lib/templates status` не показывает локальных изменений; +3. `git submodule status` не начинается с `+`, `-` или `U`; +4. `git diff --submodule=log <целевая-ветка>...HEAD` показывает ожидаемый набор + commits библиотек; +5. тесты `templates`, сборка основного проекта и проверки на целевой плате + прошли; +6. в основном проекте закоммичены необходимые изменения адаптеров, конфигурации + и сам новый gitlink. + +После получения ветки другой разработчик восстанавливает ровно выбранное +состояние одной командой: + +```bash +git pull +git submodule update --init --recursive +``` diff --git a/README.md b/README.md index e8a609c..af8cf98 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,11 @@ templates/ В ней отдельно описаны граница ядра, ABI, память, три wire format и состояние портов Windows, Android, Linux и MCU. +Обзор всего репозитория, включая слияние при подключении через submodule: +[`doc/index.html`](doc/index.html). Справочник Doxygen собирается командой +`powershell -ExecutionPolicy Bypass -File doc/build-doxygen.ps1` в +`.codex-build/doxygen/html/index.html`. + ## Как подключить к проекту **Сабмодуль** — когда нужна одна конкретная версия и обновление по команде: @@ -78,6 +83,10 @@ git submodule add https://git.rd12.ru/Andrey/templates.git lib/templates git submodule update --init --recursive ``` +Как обновлять зафиксированную ревизию, переносить изменения из проекта и +разрешать конфликт gitlink при слиянии веток, подробно описано в разделе +[`Слияние и обновление templates как Git submodule`](NEW_PROJECT.md#слияние-и-обновление-templates-как-git-submodule). + Дальше в сборку добавляются только нужные каталоги: ``` diff --git a/doc/build-doxygen.ps1 b/doc/build-doxygen.ps1 new file mode 100644 index 0000000..4207978 --- /dev/null +++ b/doc/build-doxygen.ps1 @@ -0,0 +1,28 @@ +[CmdletBinding()] +param() + +$ErrorActionPreference = 'Stop' +$repositoryRoot = Split-Path -Parent $PSScriptRoot +$doxygen = Get-Command doxygen -ErrorAction SilentlyContinue + +if (-not $doxygen) { + throw 'Doxygen не найден в PATH. Установите Doxygen и повторите запуск.' +} + +Push-Location $repositoryRoot +try { + & $doxygen.Source Doxyfile + if ($LASTEXITCODE -ne 0) { + throw "Doxygen завершился с кодом $LASTEXITCODE." + } +} +finally { + Pop-Location +} + +$indexPath = Join-Path $repositoryRoot '.codex-build\doxygen\html\index.html' +if (-not (Test-Path -LiteralPath $indexPath)) { + throw "Doxygen не создал ожидаемый файл: $indexPath" +} + +Write-Host "[DONE] Doxygen HTML: $indexPath" diff --git a/doc/index.html b/doc/index.html index c4861e4..8c58422 100644 --- a/doc/index.html +++ b/doc/index.html @@ -5,11 +5,26 @@ :root{color-scheme:dark;--bg:#07111f;--panel:#0d1b2c;--panel2:#12243a;--line:#263b54;--text:#e8f0f8;--muted:#9eb0c4;--brand:#55d6be;--blue:#8bc5ff;--warn:#fbbf24}*{box-sizing:border-box}body{margin:0;color:var(--text);background:radial-gradient(circle at 85% 5%,#143354 0,transparent 28%),var(--bg);font:15px/1.65 "Segoe UI",Arial,sans-serif}a{color:var(--blue);text-decoration:none}a:hover{text-decoration:underline}code,pre{font-family:Consolas,"Courier New",monospace}code{color:#b8f7e9}pre{overflow:auto;padding:16px;border:1px solid var(--line);border-radius:12px;background:#071521;color:#d6e5f3}.wrap{width:min(1180px,calc(100% - 32px));margin:auto}header{padding:64px 0 30px}.eyebrow{color:var(--brand);font-weight:800;letter-spacing:.12em;text-transform:uppercase;font-size:12px}h1{max-width:800px;margin:12px 0;font-size:clamp(36px,6vw,66px);line-height:1.05;letter-spacing:-.045em}h2{margin:0 0 8px;font-size:28px}h3{margin:0 0 7px;font-size:18px}.lead{max-width:850px;color:var(--muted);font-size:18px}.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-top:28px}.stat{padding:15px 18px;border:1px solid var(--line);border-radius:14px;background:#0d1b2ccc}.stat b{display:block;color:var(--brand);font-size:24px}.stat span,.card p,.head p{color:var(--muted)}.tabbar{position:sticky;top:0;z-index:10;border-block:1px solid var(--line);background:#07111fe8;backdrop-filter:blur(14px)}.tabs{display:flex;gap:6px;padding:10px 0;overflow-x:auto}.tab{border:0;border-radius:9px;padding:10px 15px;color:var(--muted);background:transparent;font:inherit;font-weight:700;white-space:nowrap;cursor:pointer}.tab:hover{color:var(--text);background:var(--panel)}.tab[aria-selected=true]{color:#07111f;background:var(--brand)}main{padding:34px 0 70px}.pane[hidden],.card[hidden]{display:none}.head{display:flex;justify-content:space-between;gap:24px;align-items:end;margin-bottom:22px}.head p{max-width:720px}.grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.card{min-width:0;padding:20px;border:1px solid var(--line);border-radius:16px;background:linear-gradient(145deg,#12243af0,#0d1b2ceb);box-shadow:0 18px 55px #00000047}.meta,.links{display:flex;flex-wrap:wrap;gap:8px}.meta{margin:14px 0}.tag{padding:3px 8px;border:1px solid #34516f;border-radius:999px;color:#bdd0e3;font-size:12px}.ready{border-color:#277463;color:#82e7d2}.links{padding-top:9px;border-top:1px solid var(--line)}.search{width:min(360px,100%);padding:11px 14px;border:1px solid var(--line);border-radius:10px;outline:0;color:var(--text);background:var(--panel);font:inherit}.search:focus{border-color:var(--brand)}.empty{display:none;padding:32px;text-align:center;color:var(--muted)}.callout{margin:18px 0;padding:17px 19px;border-left:4px solid var(--brand);border-radius:0 12px 12px 0;background:var(--panel)}.warning{border-left-color:var(--warn)}.steps{counter-reset:step;display:grid;gap:12px}.step{position:relative;padding:20px 20px 20px 70px;border:1px solid var(--line);border-radius:14px;background:var(--panel)}.step:before{counter-increment:step;content:counter(step);position:absolute;left:20px;top:20px;display:grid;place-items:center;width:32px;height:32px;border-radius:50%;color:#07111f;background:var(--brand);font-weight:900}table{width:100%;border-collapse:collapse;margin:18px 0}th,td{padding:11px 13px;border:1px solid var(--line);text-align:left;vertical-align:top}th{color:var(--brand);background:var(--panel2)}td{background:#0d1b2cad}footer{padding:25px 0 45px;color:var(--muted);border-top:1px solid var(--line)}@media(max-width:900px){.grid{grid-template-columns:repeat(2,1fr)}.stats{grid-template-columns:repeat(2,1fr)}}@media(max-width:620px){.grid{grid-template-columns:1fr}.head{display:block}.search{margin-top:12px}header{padding-top:40px}}
Embedded library catalog

Переносимые библиотеки без привязки к плате

Единый справочник по репозиторию templates: назначение модулей, границы платформенного кода и практический маршрут переноса на новый микроконтроллер.

11библиотек C99
1Python-пакет
0обязательных HAL/OS
3.9+версия Python
- +

О репозитории

Повторно используемый код прошивок и инструментов. Каждая библиотека подключается отдельно и хранит состояние у вызывающей стороны.

Ядро независимо

В переносимом слое нет заголовков MCU, HAL, регистров, IRQ, ОС и динамической памяти.

Порт узкий

SPI, GPIO, I²C, UART, CAN, Flash и часы передаются через callbacks или небольшой файл порта.

Подключение точечное

В сборку добавляются только исходники выбранной библиотеки, include-каталог и нужный порт.

Главное правило: распиновка, частоты шин и периферия принадлежат плате. Они остаются в board/ или port/, но не попадают в ядро.

Как добавить в проект

Для фиксируемой версии предпочтителен Git submodule:

git submodule add https://git.rd12.ru/Andrey/templates.git lib/templates
 git submodule update --init --recursive

Если изменения регулярно возвращаются в библиотеку, используйте subtree:

git subtree add --prefix lib/templates https://git.rd12.ru/Andrey/templates.git master --squash
 git subtree pull --prefix lib/templates https://git.rd12.ru/Andrey/templates.git master --squash
Не копируйте исходники вручную: копия быстро расходится с источником и перестаёт получать исправления.
+