Volkov: добавить Sprinter Commander

Реализовать двухпанельный Commander от платформенного PoC до этапов P6-P20: EMM-каталог, сортировку и выбор, операции с файлами и деревьями, транзакционное копирование, метаданные, политику конфликтов и предварительную проверку свободного места.

Добавить проектную документацию, HDD/MAME-сценарии и проверенные артефакты. Расширить libc операцией bank_write_page, исправлением режима O_RDONLY и связанными регрессионными проверками.
This commit is contained in:
2026-09-10 10:45:30 +03:00
parent 05bcd8197e
commit 8e389c03f8
870 changed files with 21310 additions and 25 deletions
@@ -0,0 +1,32 @@
# P1 platform probe для Sprinter Commander.
#
# Собирает два файла:
# p1probe.exe — интерактивные пробы каталогов, EMM, экрана, клавиатуры,
# DSKINFO и EXEC;
# p1child.exe — дочерняя программа для проверки EXEC/возврата.
#
# Пробы намеренно собираются в small: это изолированный диагностический
# инструмент, а не topology будущего Commander. Сборка big проверяется в P2.
PROJ_ROOT := $(abspath $(CURDIR)/../../../..)
EXAMPLE := p1probe
MEMORY := small
ALLOCS ?= 3000
EXTRA_FLAGS := --safe --max-allocs $(ALLOCS)
EXTRA_SRCS := p1_sys.c p1_emm.c p1_dir.c p1_screen.c p1_keyboard.c p1_exec_test.c
EXTRA_DATA := p1child.exe
include $(PROJ_ROOT)/app.mk
# EXEC-тест ожидает дочерний EXE рядом с основной пробой.
$(EXE): Makefile p1child.exe
p1child.exe: p1child.c
$(SPRINTER_CC) --memory tiny --safe --max-allocs $(ALLOCS) -o $@ $<
clean-child:
rm -f p1child.exe
clean: clean-child
.PHONY: clean-child
@@ -0,0 +1,25 @@
-- Диагностическая проба полей AT-клавиатуры MAME.
-- Нужна для воспроизводимого ввода навигационных и функциональных клавиш.
local ready = false
local done = false
emu.add_machine_reset_notifier(function()
ready = true
end)
emu.register_periodic(function()
if not ready or done then return end
done = true
for tag, port in pairs(manager.machine.ioport.ports) do
if tostring(tag):find(":kbd:ms_naturl:P", 1, true) then
for name, field in pairs(port.fields) do
print(string.format("[p1-key] %s mask=0x%02X name=%s",
tostring(tag), field.mask, tostring(name)))
end
end
end
manager.machine:exit()
end)
@@ -0,0 +1,147 @@
-- Автоматический MAME-сценарий для P1-пробы клавиатуры.
-- Ввод идёт прямо в поля AT-клавиатуры Sprinter, как в
-- toolchain/mame_interactive.py, но дополнен именованными клавишами.
local KB = ":kbd:ms_naturl:"
local HOLD = 0.06
local events = {}
local function add(t, tag, mask, value)
events[#events + 1] = {t, KB .. tag, mask, value}
end
local function key(t, tag, mask)
add(t, tag, mask, 1)
add(t + HOLD, tag, mask, 0)
end
local function chord(t, mod_tag, mod_mask, key_tag, key_mask)
add(t, mod_tag, mod_mask, 1)
add(t + 0.02, key_tag, key_mask, 1)
add(t + 0.08, key_tag, key_mask, 0)
add(t + 0.10, mod_tag, mod_mask, 0)
end
local chars = {
a={"P1.6",0x04}, b={"P1.5",0x10}, e={"P1.4",0x20},
o={"P2.2",0x02}, p={"P2.3",0x20}, r={"P1.5",0x01},
x={"P1.2",0x10}, ["1"]={"P1.6",0x01},
[" "]={"P2.4",0x80}, ["."]={"P2.2",0x10},
["\\"]={"P2.1",0x04}, ["\n"]={"P2.1",0x10},
[":"]={"P2.3",0x02,true}
}
local function type_text(t, text)
for i = 1, #text do
local ch = text:sub(i, i)
local def = chars[ch]
local shifted = false
if not def then
local lower = ch:lower()
def = chars[lower]
shifted = ch ~= lower
else
shifted = def[3] or false
end
if shifted then
chord(t, "P1.7", 0x02, def[1], def[2])
else
key(t, def[1], def[2])
end
t = t + 0.14
end
end
-- Запуск P1PROBE.EXE из приглашения DSS и выбор клавиатурной пробы.
type_text(8.0, "a:\\P1PROBE .EXE\n")
key(14.0, "P1.5", 0x40) -- 4
-- 24 события: навигация, F1..F10, комбинации Commander и завершающий Esc.
local sequence = {
{"P2.4",0x01}, -- Up
{"P2.3",0x10}, -- Down
{"P2.1",0x08}, -- Left
{"P2.4",0x40}, -- Right
{"P2.5",0x01}, -- Home
{"P2.5",0x20}, -- End
{"P1.2",0x01}, -- Page Up
{"P1.2",0x20}, -- Page Down
{"P2.6",0x01}, -- Insert
{"P2.6",0x02}, -- Delete
{"P1.2",0x40}, -- F1
{"P1.2",0x80}, -- F2
{"P2.6",0x40}, -- F3
{"P2.6",0x80}, -- F4
{"P2.1",0x40}, -- F5
{"P2.1",0x80}, -- F6
{"P2.2",0x40}, -- F7
{"P2.2",0x80}, -- F8
{"P2.3",0x40}, -- F9
{"P2.3",0x80} -- F10
}
for i, def in ipairs(sequence) do
key(16.0 + (i - 1) * 0.35, def[1], def[2])
end
chord(23.00, "P1.1", 0x02, "P1.5", 0x01) -- Ctrl+R
chord(23.35, "P1.1", 0x02, "P1.2", 0x01) -- Ctrl+PgUp
chord(23.70, "P1.3", 0x04, "P1.2", 0x40) -- Alt+F1
key(24.05, "P1.6", 0x40) -- Esc
-- Закрыть итоговый экран и подтвердить возврат к главному меню.
key(27.5, "P2.1", 0x10)
table.sort(events, function(a, b) return a[1] < b[1] end)
local snap_times = {17.8, 20.6, 23.4, 25.2, 29.0}
local snaps_done = {}
local start_time = nil
local ports = nil
local index = 1
local field_cache = {}
local function now()
local t = manager.machine.time
return t.seconds + t.attoseconds / 1e18
end
local function field(tag, mask)
local cache_key = tag .. "/" .. mask
if field_cache[cache_key] then return field_cache[cache_key] end
for _, candidate in pairs(ports[tag].fields) do
if candidate.mask == mask then
field_cache[cache_key] = candidate
return candidate
end
end
error("Не найдено поле " .. cache_key)
end
emu.add_machine_reset_notifier(function()
start_time = now()
end)
emu.register_periodic(function()
if not start_time then return end
if not ports then ports = manager.machine.ioport.ports end
local elapsed = now() - start_time
while index <= #events and elapsed >= events[index][1] do
local event = events[index]
field(event[2], event[3]):set_value(event[4])
index = index + 1
end
for _, snap_time in ipairs(snap_times) do
if not snaps_done[snap_time] and elapsed >= snap_time then
snaps_done[snap_time] = true
manager.machine.video:snapshot()
print("[p1-kbd] snapshot t=" .. elapsed)
end
end
if elapsed >= 31.0 then
print("[p1-kbd] exit t=" .. elapsed)
manager.machine:exit()
end
end)
@@ -0,0 +1,173 @@
/*
* p1_dir.c — наблюдаемая проверка CURDIR/CHDIR/F_FIRST/F_NEXT.
*
* Тест не меняет файлы. chdir(".") используется только для проверки
* сохранения текущего каталога.
*/
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <dir.h>
#include "p1_platform.h"
#define P1_FIND_ATTRS (FA_RDONLY | FA_HIDDEN | FA_SYSTEM | FA_DIREC | FA_ARCH)
#define P1_FIND_LIMIT 4096u
static char cwd_before[256];
static char cwd_after[256];
static ffblk_t find_buf;
static int scan_pattern(const char *pattern, uint16_t *out_count,
uint8_t *out_dot, uint8_t *out_dotdot)
{
uint16_t count = 0;
uint8_t dot = 0;
uint8_t dotdot = 0;
int rc;
int end_errno;
errno = 0;
rc = ffirst(pattern, &find_buf, P1_FIND_ATTRS);
if (rc != 0) {
if (errno == ENOENT) {
printf("Pattern %-4s: empty (ffirst errno=%d)\n", pattern, errno);
*out_count = 0;
*out_dot = 0;
*out_dotdot = 0;
return 0;
}
printf("ffirst(%s) failed: errno=%d\n", pattern, errno);
return -1;
}
for (;;) {
if (strcmp(find_buf.found_name, ".") == 0) dot = 1;
if (strcmp(find_buf.found_name, "..") == 0) dotdot = 1;
if (count < 20) {
printf(" %02X %10lu %04X/%04X %s\n",
(unsigned)find_buf.found_attr,
(unsigned long)find_buf.size,
(unsigned)find_buf.date,
(unsigned)find_buf.time,
find_buf.found_name);
}
count++;
if (count >= P1_FIND_LIMIT) {
puts("Enumeration limit reached: possible F_NEXT loop.");
return -1;
}
errno = 0;
if (fnext(&find_buf) != 0) break;
}
end_errno = errno;
printf("Pattern %-4s: count=%u dot=%u dotdot=%u end_errno=%d\n",
pattern, (unsigned)count, (unsigned)dot, (unsigned)dotdot,
end_errno);
/* MAME 0.287 / BIOS 3.06 подтвердил ENOENT (3) для конца F_NEXT.
* Код 0x0F из старого описания DSS здесь не является штатным. */
if (end_errno != ENOENT) {
puts("Unexpected F_NEXT termination code.");
return -1;
}
*out_count = count;
*out_dot = dot;
*out_dotdot = dotdot;
return 0;
}
int p1_test_directory(void)
{
uint16_t count_star_dot;
uint16_t count_star;
uint8_t dot1, dotdot1, dot2, dotdot2;
int failed = 0;
p1_heading("Directory: CURDIR, CHDIR, F_FIRST/F_NEXT");
errno = 0;
if (getcwd(cwd_before, sizeof(cwd_before)) == 0) {
printf("getcwd failed: errno=%d\n", errno);
return -1;
}
printf("CWD before: %s\n", cwd_before);
if (chdir(".") != 0) {
printf("chdir(.) failed: errno=%d\n", errno);
failed++;
}
if (getcwd(cwd_after, sizeof(cwd_after)) == 0) {
printf("second getcwd failed: errno=%d\n", errno);
return -1;
}
printf("CWD after : %s\n", cwd_after);
if (strcmp(cwd_before, cwd_after) != 0) {
puts("CWD changed after chdir(.).");
failed++;
}
puts("\nFirst entries for *.*:");
if (scan_pattern("*.*", &count_star_dot, &dot1, &dotdot1) != 0) {
failed++;
}
puts("\nFirst entries for *:");
if (scan_pattern("*", &count_star, &dot2, &dotdot2) != 0) {
failed++;
}
if (count_star != count_star_dot) {
printf("NOTE: * count (%u) differs from *.* count (%u).\n",
(unsigned)count_star, (unsigned)count_star_dot);
}
if (dot1 != dot2 || dotdot1 != dotdot2) {
puts("NOTE: dot entries differ between patterns.");
}
printf("Directory checks failed: %u\n", (unsigned)failed);
return failed ? -1 : 0;
}
int p1_test_disk_info(void)
{
P1DiskInfo info;
uint32_t cluster_bytes;
uint32_t total_bytes;
uint32_t free_bytes;
p1_heading("DSKINFO: current disk");
errno = 0;
if (p1_disk_info(0xFF, &info) != 0) {
printf("DSKINFO failed: errno=%d\n", errno);
return -1;
}
cluster_bytes = (uint32_t)info.sectors_per_cluster *
(uint32_t)info.bytes_per_sector;
total_bytes = cluster_bytes * (uint32_t)info.total_clusters;
free_bytes = cluster_bytes * (uint32_t)info.free_clusters;
printf("Sectors/cluster : %u\n", (unsigned)info.sectors_per_cluster);
printf("Bytes/sector : %u\n", (unsigned)info.bytes_per_sector);
printf("Total clusters : %u\n", (unsigned)info.total_clusters);
printf("Free clusters : %u\n", (unsigned)info.free_clusters);
printf("Cluster bytes : %lu\n", (unsigned long)cluster_bytes);
printf("Total bytes : %lu\n", (unsigned long)total_bytes);
printf("Free bytes : %lu\n", (unsigned long)free_bytes);
if (info.sectors_per_cluster == 0 || info.bytes_per_sector == 0 ||
info.total_clusters == 0 || info.free_clusters > info.total_clusters) {
puts("DSKINFO returned inconsistent values.");
return -1;
}
return 0;
}
@@ -0,0 +1,96 @@
/*
* p1_emm.c — проверка выделения трёх EMM-страниц и доступа через W3.
*/
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <errno.h>
#include <sprinter_mem.h>
#include "p1_platform.h"
#define P1_PAGE_SIZE 0x4000u
#define P1_BLOCK 64u
static uint8_t write_buf[P1_BLOCK];
static uint8_t read_buf[P1_BLOCK];
static void make_pattern(uint8_t page_index, uint8_t salt)
{
uint8_t i;
for (i = 0; i < P1_BLOCK; i++) {
write_buf[i] = (uint8_t)(salt ^ (uint8_t)(page_index * 0x41u + i));
read_buf[i] = 0;
}
}
static int check_region(uint8_t page, uint8_t page_index,
uint16_t offset, uint8_t salt)
{
make_pattern(page_index, salt);
bank_write(page, offset, write_buf, P1_BLOCK);
bank_read(page, offset, read_buf, P1_BLOCK);
if (memcmp(write_buf, read_buf, P1_BLOCK) != 0) {
printf("Mismatch: logical=%u phys=%02X offset=%04X\n",
(unsigned)page_index, (unsigned)page, (unsigned)offset);
return -1;
}
return 0;
}
int p1_test_emm(void)
{
uint16_t total_before, free_before;
uint16_t total_after, free_after;
uint8_t block;
uint8_t page[3];
uint8_t i;
int failed = 0;
p1_heading("EMM: 3-page allocation and W3 access");
errno = 0;
mem_info(&total_before, &free_before);
printf("Before: total=%u free=%u pages\n",
(unsigned)total_before, (unsigned)free_before);
block = mem_alloc_pages(3);
if (block == 0) {
printf("mem_alloc_pages(3) failed: errno=%d\n", errno);
return -1;
}
printf("Block id: %u\n", (unsigned)block);
for (i = 0; i < 3; i++) {
page[i] = mem_get_page(block, i);
printf(" logical %u -> physical %02X\n",
(unsigned)i, (unsigned)page[i]);
if (page[i] == 0) failed++;
}
if (page[0] == page[1] || page[0] == page[2] || page[1] == page[2]) {
puts("Physical page numbers are not distinct.");
failed++;
}
for (i = 0; i < 3; i++) {
if (page[i] == 0) continue;
if (check_region(page[i], i, 0, 0x5A) != 0) failed++;
if (check_region(page[i], i,
(uint16_t)(P1_PAGE_SIZE - P1_BLOCK), 0xA5) != 0) {
failed++;
}
}
mem_free_block(block);
mem_info(&total_after, &free_after);
printf("After : total=%u free=%u pages\n",
(unsigned)total_after, (unsigned)free_after);
if (total_before != total_after || free_before != free_after) {
puts("EMM counters were not restored after free.");
failed++;
}
printf("EMM checks failed: %u\n", (unsigned)failed);
return failed ? -1 : 0;
}
@@ -0,0 +1,116 @@
/*
* p1_exec_test.c — проверка EXEC, возврата к родителю и WAIT.
*/
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <conio.h>
#include <sprinter.h>
#include "p1_platform.h"
typedef struct {
int rc;
int error;
uint8_t exit_code;
uint8_t wait_code;
uint8_t page_w1;
uint8_t page_w2;
uint8_t page_w3;
char cwd[256];
} P1ExecResult;
static P1ExecResult short_result;
static P1ExecResult path_result;
static char cwd_before[256];
static void run_exec_case(const char *label, const char *path,
uint8_t path_mode, P1ExecResult *result)
{
p1_heading(label);
printf("Calling EXEC: B=%u, path=%s\n", (unsigned)path_mode, path);
cputs("The child switches to 40x32 and waits for one key.\r\n");
cputs("After that it returns code 0x5A.\r\n\r\n");
cputs("Press any key to call EXEC...");
(void)getkey();
errno = 0;
result->rc = p1_exec(path_mode, path);
result->error = errno;
result->exit_code = p1_exec_exit_code;
result->wait_code = p1_wait_code();
/* Сначала снять страницы, затем исправлять видеорежим и печатать. */
result->page_w1 = _io_page_w1;
result->page_w2 = _io_page_w2;
result->page_w3 = _io_page_w3;
if (getcwd(result->cwd, sizeof(result->cwd)) == 0) {
result->cwd[0] = 0;
}
(void)settextmode(TEXT_MODE_80x32);
}
static void print_result(const char *name, const P1ExecResult *result)
{
printf("%s: rc=%d errno=%d exit=%02X wait=%02X pages=%02X/%02X/%02X\n",
name, result->rc, result->error,
(unsigned)result->exit_code, (unsigned)result->wait_code,
(unsigned)result->page_w1, (unsigned)result->page_w2,
(unsigned)result->page_w3);
printf(" cwd=%s\n", result->cwd[0] ? result->cwd : "<getcwd failed>");
}
int p1_test_exec(void)
{
uint8_t before_w1 = _io_page_w1;
uint8_t before_w2 = _io_page_w2;
uint8_t before_w3 = _io_page_w3;
int failed = 0;
if (getcwd(cwd_before, sizeof(cwd_before)) == 0) {
p1_heading("EXEC: setup failed");
printf("getcwd failed: errno=%d\n", errno);
return -1;
}
/* B=0 характеризуем для будущей командной строки. Ошибка этого случая
* не блокирует PoC: Commander будет передавать явный путь с B=1. */
run_exec_case("EXEC case 1: short name / PATH",
"P1CHILD.EXE", 0, &short_result);
/* B=1 и явный относительный путь — обязательный случай PoC. */
run_exec_case("EXEC case 2: explicit path",
".\\P1CHILD.EXE", 1, &path_result);
p1_heading("EXEC results");
printf("Parent before: pages=%02X/%02X/%02X cwd=%s\n",
(unsigned)before_w1, (unsigned)before_w2, (unsigned)before_w3,
cwd_before);
print_result("B=0", &short_result);
print_result("B=1", &path_result);
if (path_result.rc != 0 || path_result.exit_code != 0x5A ||
path_result.wait_code != 0x5A) {
puts("Required B=1 EXEC result is incorrect.");
failed++;
}
if (path_result.page_w1 != before_w1 || path_result.page_w2 != before_w2 ||
path_result.page_w3 != before_w3) {
puts("Parent window mapping was not restored.");
failed++;
}
if (strcmp(path_result.cwd, cwd_before) != 0) {
puts("Parent CWD changed after child.");
failed++;
}
if (short_result.rc != 0) {
puts("NOTE: B=0 short-name lookup failed; explicit B=1 remains usable.");
}
printf("EXEC checks failed: %u\n", (unsigned)failed);
return failed ? -1 : 0;
}
@@ -0,0 +1,45 @@
/*
* p1_keyboard.c — печать сырых событий getkey()/kbd_mod_state().
*/
#include <stdio.h>
#include <stdint.h>
#include <conio.h>
#include "p1_platform.h"
static uint8_t compact_modifiers(uint16_t state)
{
uint8_t result = 0;
if (state & (KBD_MOD_LSHIFT | KBD_MOD_RSHIFT)) result |= 0x01;
if (state & (KBD_MOD_CTRL | KBD_MOD_LCTRL | KBD_MOD_RCTRL)) result |= 0x02;
if (state & (KBD_MOD_ALT | KBD_MOD_LALT | KBD_MOD_RALT)) result |= 0x04;
return result;
}
int p1_test_keyboard(void)
{
uint8_t n = 0;
p1_heading("Keyboard: raw event capture");
cputs("Press navigation keys, F1..F10 and Ctrl combinations.\r\n");
cputs("Esc finishes after its event is printed. Maximum: 24 events.\r\n\r\n");
cputs(" n key ascii scan live_mod compact\r\n");
while (n < 24) {
uint16_t key = getkey();
uint16_t live = kbd_mod_state();
uint8_t ascii = (uint8_t)key;
uint8_t scan = (uint8_t)(key >> 8);
uint8_t compact = compact_modifiers(live);
char printable = (ascii >= 0x20 && ascii < 0x7F) ? (char)ascii : '.';
printf("%2u %c %02X %02X %04X %02X\n",
(unsigned)n, printable, (unsigned)ascii, (unsigned)scan,
(unsigned)live, (unsigned)compact);
n++;
if (ascii == 0x1B) break;
}
cputs("\r\nCompare the captured values with commander-keymap.md.\r\n");
return 0;
}
@@ -0,0 +1,43 @@
/*
* p1_platform.h — общий контракт платформенных проб Commander P1.
*
* Это диагностический код приложения, не публичный заголовок libc. После
* подтверждения в MAME стабильные wrappers либо останутся в Commander,
* либо будут перенесены в libc отдельными модулями и тестами.
*/
#ifndef P1_PLATFORM_H
#define P1_PLATFORM_H
#include <stdint.h>
typedef struct {
uint8_t sectors_per_cluster;
uint16_t total_clusters;
uint16_t free_clusters;
uint16_t bytes_per_sector;
} P1DiskInfo;
typedef char P1DiskInfo_must_be_7_bytes[(sizeof(P1DiskInfo) == 7) ? 1 : -1];
/* Последний код завершения, возвращённый EXEC при CF=0. */
extern uint8_t p1_exec_exit_code;
int p1_disk_info(uint8_t disk, P1DiskInfo *out);
int p1_exec(uint8_t path_mode, const char *path);
uint8_t p1_wait_code(void);
void p1_winrest(uint8_t row, uint8_t col, uint8_t height, uint8_t width,
uint8_t page, uint16_t offset);
int p1_test_emm(void);
int p1_test_directory(void);
int p1_test_screen(void);
int p1_test_keyboard(void);
int p1_test_exec(void);
int p1_test_disk_info(void);
void p1_heading(const char *title);
void p1_pause(void);
#endif
@@ -0,0 +1,120 @@
/*
* p1_screen.c — визуальная проверка экранной EMM-страницы и WINREST.
*/
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <errno.h>
#include <conio.h>
#include <sprinter_mem.h>
#include "p1_platform.h"
#define P1_SCREEN_W 80u
#define P1_SCREEN_H 32u
#define P1_ROW_BYTES 160u
#define P1_MARKER_OFF 0x2000u
static uint8_t row_buf[P1_ROW_BYTES];
static uint8_t marker_buf[2u * 20u * 2u];
static void row_clear(uint8_t attr)
{
uint8_t x;
for (x = 0; x < P1_SCREEN_W; x++) {
row_buf[(uint16_t)x * 2u] = ' ';
row_buf[(uint16_t)x * 2u + 1u] = attr;
}
}
static void row_text(uint8_t x, const char *text, uint8_t attr)
{
while (*text && x < P1_SCREEN_W) {
row_buf[(uint16_t)x * 2u] = (uint8_t)*text++;
row_buf[(uint16_t)x * 2u + 1u] = attr;
x++;
}
}
static void marker_build(void)
{
static const char top[] = "PARTIAL WINREST OK ";
static const char bot[] = "offset = 0x2000 ";
uint8_t x;
for (x = 0; x < 20; x++) {
marker_buf[(uint16_t)x * 2u] = (uint8_t)top[x];
marker_buf[(uint16_t)x * 2u + 1u] =
COLOR(COLOR_WHITE, COLOR_RED);
marker_buf[40u + (uint16_t)x * 2u] = (uint8_t)bot[x];
marker_buf[40u + (uint16_t)x * 2u + 1u] =
COLOR(COLOR_YELLOW, COLOR_RED);
}
}
int p1_test_screen(void)
{
uint8_t block;
uint8_t page;
uint8_t y;
p1_heading("WINREST: preparing EMM screen");
errno = 0;
block = mem_alloc_pages(1);
if (block == 0) {
printf("mem_alloc_pages failed: errno=%d\n", errno);
return -1;
}
page = mem_get_page(block, 0);
if (page == 0) {
printf("mem_get_page failed: errno=%d\n", errno);
mem_free_block(block);
return -1;
}
for (y = 0; y < P1_SCREEN_H; y++) {
/* Только яркие foreground 9..15 на синем: граница не должна
* исчезать из-за комбинации blue-on-blue вроде прежнего 0x11. */
uint8_t attr = COLOR((uint8_t)(COLOR_LIGHTBLUE + (y % 7u)),
COLOR_BLUE);
row_clear(attr);
row_buf[0] = (y == 0 || y == 31) ? '+' : '|';
row_buf[158] = (y == 0 || y == 31) ? '+' : '|';
if (y == 0 || y == 31) {
uint8_t x;
for (x = 1; x < 79; x++) row_buf[(uint16_t)x * 2u] = '-';
}
if (y == 2) row_text(4, "FULL 80x32 WINREST FROM EMM PAGE",
COLOR(COLOR_WHITE, COLOR_BLUE));
if (y == 4) row_text(4, "Rows have different foreground colours.",
COLOR(COLOR_YELLOW, COLOR_BLUE));
if (y == 6) row_text(4, "Expected: complete border, no shifted cells.",
COLOR(COLOR_YELLOW, COLOR_BLUE));
if (y == 28) row_text(4, "Press a key for partial-offset test.",
COLOR(COLOR_WHITE, COLOR_BLUE));
bank_write(page, (uint16_t)y * P1_ROW_BYTES,
row_buf, P1_ROW_BYTES);
}
marker_build();
bank_write(page, P1_MARKER_OFF, marker_buf, sizeof(marker_buf));
p1_winrest(0, 0, P1_SCREEN_H, P1_SCREEN_W, page, 0);
(void)getkey();
/* Две строки по 20 ячеек поверх середины полного экрана. */
p1_winrest(14, 30, 2, 20, page, P1_MARKER_OFF);
(void)getkey();
mem_free_block(block);
p1_heading("WINREST test finished");
cputs("Visual checks:\r\n");
cputs(" 1. Full 80x32 frame had no shifts or holes.\r\n");
cputs(" 2. Red/yellow 20x2 marker appeared at row 14, col 30.\r\n");
cputs(" 3. Marker text started at offset 0x2000.\r\n");
cputs("\r\nReturn PASS only records that both calls completed.\r\n");
return 0;
}
@@ -0,0 +1,143 @@
/*
* p1_sys.c — отсутствующие в libc сырые ESTEX wrappers для проб P1.
*
* ABI SDCC __sdcccall(1): int возвращается в DE, uint8_t — в A. Каждый RST
* сохраняет IX, потому что IX callee-saved. Ошибка ESTEX (CF=1, код в A)
* проходит через общий libc-хелпер __errno_set.
*/
#include <stdint.h>
#include "p1_platform.h"
uint8_t p1_exec_exit_code;
/* ESTEX DSKINFO $03:
* A=disk (0xFF=current)
* out: A=sectors/cluster, HL=total clusters,
* DE=free clusters, BC=bytes/sector.
*/
int p1_disk_info(uint8_t disk, P1DiskInfo *out) __naked
{
(void)disk;
(void)out;
__asm
;; A = disk, DE = out.
push ix
push de
ld c, #0x03
rst #0x10
jr c, _p1_di_error
;; Забрать out в IX, пока все выходные регистры DSS ещё целы.
pop ix
ld 0 (ix), a
ld 1 (ix), l
ld 2 (ix), h
ld 3 (ix), e
ld 4 (ix), d
ld 5 (ix), c
ld 6 (ix), b
pop ix
ld de, #0
ret
_p1_di_error:
pop de ; снять сохранённый out
pop ix
call __errno_set
ld de, #-1
ret
__endasm;
}
/* ESTEX EXEC $40. path_mode: 0 — короткое имя/PATH, 1 — передан путь.
* На успехе код дочерней программы сохраняется в p1_exec_exit_code.
*/
int p1_exec(uint8_t path_mode, const char *path) __naked
{
(void)path_mode;
(void)path;
__asm
;; A = path_mode, DE = path. Такой порядок не оставляет 8-битный
;; второй аргумент на стеке в ABI SDCC.
push ix
ld b, a
ex de, hl
ld c, #0x40
rst #0x10
pop ix
jr c, _p1_exec_error
ld (_p1_exec_exit_code), a
ld de, #0
ret
_p1_exec_error:
call __errno_set
ld de, #-1
ret
__endasm;
}
/* ESTEX WAIT $42 возвращает код последней завершившейся программы в A. */
uint8_t p1_wait_code(void) __naked
{
__asm
push ix
ld c, #0x42
rst #0x10
pop ix
ret
__endasm;
}
/* ESTEX WINREST $5A:
* D=row, E=col, H=height, L=width, B=physical page,
* IX=0xC000+offset.
*
* Раскладка аргументов подтверждена существующим tests/winrest и mdview2:
* A=row, L=col, стек +2 height, +3 width, +4 page, +5..6 offset.
*/
void p1_winrest(uint8_t row, uint8_t col, uint8_t height, uint8_t width,
uint8_t page, uint16_t offset) __naked
{
(void)row;
(void)col;
(void)height;
(void)width;
(void)page;
(void)offset;
__asm
ld iy, #2
add iy, sp
ld d, a
ld e, l
push ix
ld l, 3 (iy)
ld h, 4 (iy)
ld bc, #0xC000
add hl, bc
push hl
pop ix
ld h, 0 (iy)
ld a, 1 (iy)
ld l, a
ld a, 2 (iy)
ld b, a
ld c, #0x5A
di
rst #0x10
ei
pop ix
;; Снять 5 стековых байтов height,width,page,offset.
pop hl
inc sp
inc sp
inc sp
inc sp
inc sp
jp (hl)
__endasm;
}
@@ -0,0 +1,35 @@
/*
* p1child.c — дочерняя программа для проверки ESTEX EXEC.
*
* Намеренно меняет видеорежим, ничего не пишет на диск и возвращает 0x5A.
*/
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
#include <conio.h>
#include <sprinter.h>
static char child_cwd[256];
int main(void)
{
(void)settextmode(TEXT_MODE_40x32);
clrscr_attr(0x27);
gotoxy(0, 0);
textattr(0x2F);
cputs("P1CHILD.EXE\r\n\r\n");
textattr(0x27);
cputs("EXEC reached child successfully.\r\n");
cprintf("W1/W2/W3: %02X/%02X/%02X\r\n",
(unsigned)_io_page_w1, (unsigned)_io_page_w2,
(unsigned)_io_page_w3);
if (getcwd(child_cwd, sizeof(child_cwd)) != 0) {
cprintf("CWD: %s\r\n", child_cwd);
} else {
cputs("CWD: <getcwd failed>\r\n");
}
cputs("\r\nPress any key to return 0x5A...");
(void)getkey();
return 0x5A;
}
@@ -0,0 +1,117 @@
/*
* p1probe.c — интерактивный набор платформенных проб Sprinter Commander.
*
* Программа ничего не удаляет и не изменяет в текущем каталоге. EMM-тест
* пишет исключительно в выделенный блок памяти, дочерняя программа также
* не выполняет дисковых записей.
*/
#include <stdio.h>
#include <stdint.h>
#include <conio.h>
#include "p1_platform.h"
void p1_heading(const char *title)
{
clrscr_attr(0x17);
gotoxy(0, 0);
textattr(0x1F);
cprintf("Sprinter Commander - P1 platform probe\r\n");
textattr(0x17);
cprintf("%s\r\n\r\n", title);
}
void p1_pause(void)
{
textattr(0x1E);
cputs("\r\nPress any key to return to menu...");
(void)getkey();
}
static void show_menu(void)
{
p1_heading("Main menu");
cputs("1 Directory and F_FIRST/F_NEXT\r\n");
cputs("2 EMM allocation and page access\r\n");
cputs("3 WINREST full/partial screen\r\n");
cputs("4 Keyboard event codes\r\n");
cputs("5 EXEC child and return\r\n");
cputs("6 DSKINFO current disk\r\n");
cputs("A Automatic non-visual probes (1, 2, 6)\r\n");
cputs("0 Exit\r\n\r\n");
textattr(0x1E);
cputs("Select: ");
}
static void report_result(int rc)
{
if (rc == 0) {
textattr(0x2F);
cputs("\r\nRESULT: PASS\r\n");
} else {
textattr(0x4F);
cputs("\r\nRESULT: FAIL\r\n");
}
}
static void run_automatic(void)
{
int failed = 0;
p1_heading("Automatic non-visual probes");
cputs("[1/3] Directory\r\n");
if (p1_test_directory() != 0) failed++;
cputs("\r\n[2/3] EMM\r\n");
if (p1_test_emm() != 0) failed++;
cputs("\r\n[3/3] DSKINFO\r\n");
if (p1_test_disk_info() != 0) failed++;
textattr(failed ? 0x4F : 0x2F);
cprintf("\r\nAutomatic result: %u failed\r\n", (unsigned)failed);
p1_pause();
}
int main(void)
{
uint16_t key;
uint8_t ch;
int rc;
if (settextmode(TEXT_MODE_80x32) != 0) {
puts("Cannot set 80x32 text mode.");
return 1;
}
for (;;) {
show_menu();
key = getkey();
ch = (uint8_t)key;
if (ch >= 'a' && ch <= 'z') ch = (uint8_t)(ch - 'a' + 'A');
if (ch == '0' || ch == 0x1B) break;
if (ch == 'A') {
run_automatic();
continue;
}
rc = -1;
switch (ch) {
case '1': rc = p1_test_directory(); break;
case '2': rc = p1_test_emm(); break;
case '3': rc = p1_test_screen(); break;
case '4': rc = p1_test_keyboard(); break;
case '5': rc = p1_test_exec(); break;
case '6': rc = p1_test_disk_info(); break;
default: continue;
}
report_result(rc);
p1_pause();
}
textattr(0x0F);
clrscr();
return 0;
}