Polyglot Opening Book for Micro-Max on STM32

Complete Technical Reference
Version: 7.0  |  Date: October 2, 2026  |  Author: alexlaw@list.ru
Engine: micro-Max 4.5 (H.G. Müller)  |  MCU: STM32F103C8  |  IDE: IAR EW for Arm 9.30.1

Table of Contents

  1. What Is a Polyglot Book and Why It Is Needed
  2. System Architecture
  3. Conventions and Data Conversion
    1. Coordinates: 0x88 ↔ 0x3F
    2. Piece Types: Micro-Max ↔ Polyglot
    3. Colors and Side to Move
    4. Castling Rights
    5. En Passant
  4. Project Files
  5. Zobrist Hashing
  6. polyglot.c — C Implementation
  7. main.c — Integration into the Game Loop
  8. The book_pending Mechanism
  9. Book Generation (Python + Stockfish)
  10. Full Data Flow (Diagram)
  11. Troubleshooting
  12. Memory Consumption
  13. Bug History & Fixes
  14. Communication Protocol: STM32 ↔ Server

1. What Is a Polyglot Book and Why It Is Needed

Polyglot is the standard binary opening book format used by most chess engines (Stockfish, Houdini, Fruit, etc.). Each entry contains:

The micro-Max engine (by H.G. Müller) is a minimalist engine under 2 KB of source code. It plays well in the middlegame but, due to shallow search (fixed node limit), it can make suboptimal moves in the opening. An opening book solves this: the first 2–6 moves are taken from a precomputed database, then control is handed to the engine.

After integrating the book, STM32 plays the first 2–6 moves from the book (marker B in UART), then switches to the engine (marker D). This improves opening quality without increasing move time.

2. System Architecture

┌─────────────────────────────────────────────────────┐ │ STM32F103C8 │ │ │ │ ┌──────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ UART │──▶│ main.c │──▶│ D() (engine) │ │ │ │ RX/TX │ │ game loop │ │ micro-Max │ │ │ │ │◀──│ │◀──│ search │ │ │ └──────────┘ └──────┬───────┘ └──────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ polyglot.c │ │ │ │ ┌─────────────┐ ┌──────────────┐ │ │ │ │ │compute_ │ │polyglot_ │ │ │ │ │ │zobrist() │ │find_move() │ │ │ │ │ └──────┬──────┘ └──────┬───────┘ │ │ │ │ │ │ │ │ │ │ ▼ ▼ │ │ │ │ ┌────────────┐ ┌──────────────┐ │ │ │ │ │zobrist_ │ │debut_book.c │ │ │ │ │ │table.h │ │(Flash, 8 KB) │ │ │ │ │ │(781 nums) │ │1000 entries │ │ │ │ │ └────────────┘ └──────────────┘ │ │ │ └─────────────────────────────────────────────────┘ │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │ │ MAX7219 │ │ GPIO │ │ SysTick │ │ │ │ (LED │ │ (buttons)│ │ (timer) │ │ │ │ matrix) │ │ │ │ │ │ │ └──────────┘ └──────────┘ └──────────────┘ │ └─────────────────────────────────────────────────────┘ ┌─────────────────────────────┐ │ PC (Python + Stockfish) │ │ generate_strong_book.py │ │ │ │ chess.Board → Stockfish │ │ → Zobrist → debut_book.c │ └─────────────────────────────┘

The solution is split into two parts:

3. Conventions and Data Conversion

The main challenge: micro-Max and Polyglot use different coordinate systems, piece encodings, and color conventions. Without correct conversion, Zobrist hashes will not match and the book will not work.

3.1 Coordinates: 0x88 ↔ 0x3F

Micro-Max uses 0x88 notation: square = rank * 16 + file, where rank 0 = a8 (top), rank 7 = a1 (bottom). Valid squares: 0x00–0x77, odd bits 4–7 = off-board.

Polyglot (and the Python chess library) uses linear numbering 0x3F: a1=0, h1=7, a8=56, h8=63.

Square0x88 (micro-Max)0x3F (Polyglot)Python (chess lib)Formula
a80x00 (0)5656(7-0)*8+0 = 56
e10x74 (116)44(7-7)*8+4 = 4
h10x77 (119)77(7-7)*8+7 = 7
a10x70 (112)00(7-7)*8+0 = 0
d40x43 (67)2727(7-4)*8+3 = 27

C conversion (sq88_to_063):

// 0x88 → 0x3F WITH rank inversion
static int sq88_to_063(int sq) {
    return (7 - (sq >> 4)) * 8 + (sq & 7);
}

Python conversion (corrected — NO rank inversion):

# Python chess lib: square is already 0x3F, NO inversion needed
# chess.square_file(sq) = file (0-7)
# chess.square_rank(sq) = rank (0-7), rank 0 = rank 1 (white side)
sq063 = rank_ * 8 + file_   # NO (7 - rank_) inversion!
Bug fixed (v7.0): The Python script originally had (7 - rank_) * 8 + file_, which inverted the rank and caused key mismatches. The correct formula for Python is rank_ * 8 + file_ — no inversion, because the chess library already uses rank 0 = white's back rank (a1=0). The C code does need inversion because micro-Max uses rank 0 = a8 (top of board).

3.2 Piece Types: Micro-Max ↔ Polyglot

Micro-Max stores the piece type in the lower 3 bits of the b[] array. The numbering does not match the Polyglot standard.

PieceMicro-Max p & 7Standard (Polyglot)In b[]
Pawn (black)1117 (16+1)
Pawn (white)2110 (8+2)
Knight3211 / 19
King4612 / 20
Bishop5313 / 21
Rook6414 / 22
Queen7515 / 23

Conversion array mm_to_std[8]:

/*           0  1  2  3  4  5  6  7  */
/* MM type:   -  P  P  N  K  B  R  Q  */
/* Std type:  0  1  1  2  6  3  4  5  */
static const int mm_to_std[8] = {0, 1, 1, 2, 6, 3, 4, 5};
Both pawn types (1 and 2) map to standard type 1. Color is determined separately by bit 3 (white) or bit 4 (black).

3.3 Colors and Side to Move

In micro-Max, color is encoded by a bit in the square value:

Critical correction (v7.0): The Zobrist key uses white = 0, black = 1 for the color index. The original documentation stated color = 1 for white, but the working keys from the 19-entry reference book proved that white = 0 and black = 1 is correct.
SystemWhiteBlackCode
C (compute_zobrist)10(p & 8) ? 1 : 0
Python (zobrist_key_stm32)010 if piece.color else 1
Net effectBoth produce the same key because C inverts the rank (swapping visual position) while Python does not — the two inversions cancel out.

Side to move: In micro-Max, k=8 means white to move, k=16 means black to move. In the Zobrist hash, if side_val != 8 (black to move), ZobristTable[768] is XORed.

/* Side to move: k=8 → white (no XOR), k=16 → black (XOR) */
if (side_val != 8) {
    h ^= ZobristTable[768];
}

3.4 Castling Rights

Castling is encoded in the Zobrist hash by 4 separate numbers (indices 769–772):

RightIndexC ConditionSquare (0x88)Value
White, kingside (K)769b[0x77] == 14h1Rook (6+8)
White, queenside (Q)770b[0x70] == 14a1Rook (6+8)
Black, kingside (k)771b[0x07] == 22h8Rook (6+16)
Black, queenside (q)772b[0x00] == 22a8Rook (6+16)

Additionally, the king must be on its home square:

Function can_castle():

static int can_castle(int white, int kingside) {
    if (white) {
        if (b[0x74] != 12) return 0;            /* white king on e1 */
        if (kingside  && b[0x77] != 14) return 0;/* white rook on h1 */
        if (!kingside && b[0x70] != 14) return 0;/* white rook on a1 */
    } else {
        if (b[0x04] != 20) return 0;            /* black king on e8 */
        if (kingside  && b[0x07] != 22) return 0;/* black rook on h8 */
        if (!kingside && b[0x00] != 22) return 0;/* black rook on a8 */
    }
    return 1;
}

3.5 En Passant

If a pawn just moved two squares forward, micro-Max stores the “skipped” square in variable E1. In the Zobrist hash this is XORed with a number from the range 773–780 (one per file a–h):

/* En passant */
if (E1 != 0 && !(E1 & 0x88)) {
    h ^= ZobristTable[773 + (E1 & 7)];
}

4. Project Files

FilePurposeSizeMemory
zobrist_table.h781 pseudo-random 64-bit numbers for hashing~6 KBFlash
debut_book.cPosition array (key + move + weight), binary search~8 KB (1000 entries)Flash
polyglot.cFunctions: compute_zobrist, polyglot_find_move, polyglot_to_mm~2 KBFlash (code)
polyglot.hHeaders: typedef, function prototypes~0.5 KB—
main.cGame loop, book integration via book_pending~12 KBFlash (code) + RAM
generate_strong_book.pyOffline book generator (Python + Stockfish)~6 KBPC only
debug_keys.pyValidation tool: verifies Zobrist keys against known book~4 KBPC only

5. Zobrist Hashing

Zobrist hash is the XOR of pseudo-random numbers, one per position component:

Total: 781 numbers, each 64-bit.

Index Structure

index = color * 384 + (piece_type - 1) * 64 + square_063

  color:      0 = black, 1 = white (C)  /  0 = white, 1 = black (Python)
  piece_type: 1 = pawn, 2 = knight, 3 = bishop, 4 = rook, 5 = queen, 6 = king
  square:     0..63 (a1=0, h8=63)

  color * 384:   0 (first color) or 384 (second color)
  piece * 64:    0, 64, 128, 192, 256, 320
The color convention differs between C and Python, but the rank inversion also differs (C inverts, Python does not). These two differences cancel each other out, producing identical Zobrist keys. This was verified empirically using debug_keys.py against the 19-entry reference book.

C function compute_zobrist()

uint64_t compute_zobrist(int side_val) {
    uint64_t h = 0;
    int sq;

    for (sq = 0; sq < 128; sq++) {
        if (sq & 0x88) continue;      /* skip off-board */
        int p = b[sq];
        if (p == 0) continue;        /* empty square */
        int mm_type = p & 7;
        int std_type = mm_to_std[mm_type];
        int color = (p & 8) ? 1 : 0;  /* white=1, black=0 */
        int sq063 = sq88_to_063(sq);

        h ^= ZobristTable[color * 384 + (std_type - 1) * 64 + sq063];
    }

    if (side_val != 8) h ^= ZobristTable[768];  /* black to move */
    if (can_castle(1, 1)) h ^= ZobristTable[769];
    if (can_castle(1, 0)) h ^= ZobristTable[770];
    if (can_castle(0, 1)) h ^= ZobristTable[771];
    if (can_castle(0, 0)) h ^= ZobristTable[772];

    if (E1 != 0 && !(E1 & 0x88))
        h ^= ZobristTable[773 + (E1 & 7)];

    return h;
}

Python function zobrist_key_stm32() (corrected v7.0)

MM_TO_STD = [0, 1, 1, 2, 6, 3, 4, 5]

def get_mm_type(piece):
    if piece.piece_type == chess.PAWN:   return 2 if piece.color else 1
    elif piece.piece_type == chess.KNIGHT: return 3
    elif piece.piece_type == chess.BISHOP: return 5
    elif piece.piece_type == chess.ROOK:   return 6
    elif piece.piece_type == chess.QUEEN:  return 7
    elif piece.piece_type == chess.KING:   return 4
    return 0

def zobrist_key_stm32(board, ZT):
    key = 0
    for sq in chess.SQUARES:
        piece = board.piece_at(sq)
        if piece is None:
            continue
        mm_type = get_mm_type(piece)
        std_type = MM_TO_STD[mm_type]
        # FIXED: white=0, black=1
        color = 0 if piece.color else 1
        file_ = chess.square_file(sq)
        rank_ = chess.square_rank(sq)
        # FIXED: NO rank inversion
        sq063 = rank_ * 8 + file_
        z_index = color * 384 + (std_type - 1) * 64 + sq063
        key ^= ZT[z_index]
    if not board.turn:  # black to move
        key ^= ZT[768]
    # Castling
    if board.castling_rights & chess.BB_H1: key ^= ZT[769]
    if board.castling_rights & chess.BB_A1: key ^= ZT[770]
    if board.castling_rights & chess.BB_H8: key ^= ZT[771]
    if board.castling_rights & chess.BB_A8: key ^= ZT[772]
    # En passant
    if board.ep_square is not None:
        key ^= ZT[773 + chess.square_file(board.ep_square)]
    return key & 0xFFFFFFFF

Verified Keys (debug_keys.py validation)

The following 4 keys from the 19-entry reference book were reproduced exactly by the corrected Python function:

PositionKeyMove (hex)Decoded MoveMatch
After 1.e4 (black to move)0x66FF886D0x08B2c7-c5 (Sicilian)✔
After 1.c4 (black to move)0x9941BC1A0x0B7Eg8-f6 (Nf6)✔
After 1.d4 (black to move)0xD75F281A0x0B7Eg8-f6 (Nf6)✔
After 1.Nf3 (black to move)0x835FB1990x0B7Eg8-f6 (Nf6)✔
The function returns uint64_t but only the lower 32 bits (uint32_t key) are stored in debut_book.c. The probability of collision across 1000 entries is negligibly small (~10-6).

6. polyglot.c — C Implementation

The file contains three key functions. Addresses are from the IAR linker map file.

6.1 compute_zobrist(int side_val)

See Section 5. Returns the 64-bit Zobrist hash of the current position on board b[]. Address: 0x800'4663, size: 0xF8.

6.2 polyglot_find_move(uint32_t key, uint16_t *out_move)

Binary search in the sorted array debut_book[]:

int polyglot_find_move(uint32_t key, uint16_t *out_move) {
    extern const polyglot_entry_t debut_book[];
    extern const int debut_book_size;

    int l = 0, r = debut_book_size - 1;
    while (l <= r) {
        int m = (l + r) >> 1;
        if (debut_book[m].key == key) {
            *out_move = debut_book[m].move;
            return 1;       /* found */
        } else if (debut_book[m].key < key) {
            l = m + 1;
        } else {
            r = m - 1;
        }
    }
    return 0;               /* not found */
}

Address: 0x800'475B, size: 0x42.

Requirement: The debut_book[] array must be sorted by key in ascending order. The Python generator does this automatically via book.sort(key=lambda x: x[0]).

6.3 polyglot_to_mm(uint16_t pg_move, int *from88, int *to88)

Converts a Polyglot move (6+6 bits) to 0x88 coordinates for micro-Max:

void polyglot_to_mm(uint16_t pg_move, int *from88, int *to88) {
    int from063 = pg_move & 0x3F;
    int to063   = (pg_move >> 6) & 0x3F;

    *from88 = (7 - (from063 / 8)) * 16 + (from063 % 8);
    *to88   = (7 - (to063   / 8)) * 16 + (to063   % 8);
}

Address: 0x800'47C9, size: 0x48. Polyglot move: bits 0–5 = “from” square (0x3F), bits 6–11 = “to” square, bits 12–14 = promotion piece (0 = none, 1 = knight, 2 = bishop, 3 = rook, 4 = queen).

Python move encoding (corrected v7.0):

def move_to_polyglot(move):
    from_sq = move.from_square
    to_sq = move.to_square
    file_from = chess.square_file(from_sq)
    rank_from = chess.square_rank(from_sq)
    file_to = chess.square_file(to_sq)
    rank_to = chess.square_rank(to_sq)
    # FIXED: NO rank inversion
    sq063_from = rank_from * 8 + file_from
    sq063_to = rank_to * 8 + file_to
    return (sq063_from & 0x3F) | ((sq063_to & 0x3F) << 6)

7. main.c — Integration into the Game Loop

7.1 Global Variables

int side, one, book_pending = 0;   /* book_pending — book flag */
uint16_t pg_move;                /* move from book */
uint32_t book_key;              /* Zobrist key */
int from88, to88;               /* coordinates for engine */

7.2 Book Block (before the input wait loop)

/* === OPENING BOOK === */
if (book_pending) {
    book_pending = 0;
    NVIC_DisableIRQ(USART1_IRQn);
    USART1->CR1 &= ~USART_CR1_RE;

    book_key = (uint32_t)(compute_zobrist(k) & 0xFFFFFFFFU);

    if (polyglot_find_move(book_key, &pg_move)) {
        uart1_write('B');                        /* marker: book move */
        polyglot_to_mm(pg_move, &from88, &to88);

        /* Write move to c[] — as if human entered it */
        c[0] = 'a' + (from88 & 7);
        c[1] = '8' - (from88 >> 4);
        c[2] = 'a' + (to88 & 7);
        c[3] = '8' - (to88 >> 4);
        c[4] = '\n';
        data_count = -1;

        goto skip_wait;  /* skip the input wait loop */
    }

    uart1_write('D');                            /* marker: engine search */
    USART1->CR1 |= USART_CR1_RE;
    NVIC_EnableIRQ(USART1_IRQn);
}

7.3 After D() — Set Book Flag

if (side != k) {
    half_m++;

    if (side == 8) book_pending = 1;  /* white moved → black from book */

    char ssd = sd == 0 ? 'w' : 'b';
    /* ... move display ... */
}

7.4 After nxt: Label

nxt:;

side = k;
k ^= D(k, -I, I, Q, 1, j++, O, 8, 2) - I ? 0 : 24;

8. The book_pending Mechanism

Key idea: the book move is fed into the engine via the same path as a human move — through the c[] array, parsing, GameHistory validation, and the D() call. This guarantees the move is checked for legality and correctly applied.

┌─────────────────────────────────────────────────────────┐ │ ITERATION 1: HUMAN (SERVER) MOVE │ │ │ │ UART RX → rc_data → c[] → data_count > 0 │ │ → parse: K = c[0]-16*c[1]+C, L = c[2]-16*c[3]+C │ │ → GameHistory check → goto nxt │ │ → nxt: side=k; k ^= D(k,...) - I ? 0 : 24 │ │ → side==8 (white moved) → book_pending = 1 │ │ → move display on LED matrix │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ ITERATION 2: COMPUTER MOVE (BOOK OR ENGINE) │ │ │ │ │ │ │ │ book_pending == 1? │ │ │ │ ├── YES: compute_zobrist(k) → polyglot_find_move │ │ │ │ │ ├── FOUND: c[] ← book move, data_count=-1 │ │ │ │ │ │ → goto skip_wait → parse → D() │ │ │ │ │ │ → 'B' in UART │ │ │ │ │ └── NOT FOUND: 'D' in UART → enable UART │ │ │ │ │ → while(data_count==0) waits for enter/'0' │ │ │ │ │ → D() normal search │ │ │ │ └── NO: while(data_count==0) waits for input │ │ │ │ │ │ │ │ After D(): side != k → move display │ │ │ │ book_pending NOT set (side==16, black moved) │ │ │ └─────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘

Why this is reliable: The book move goes through D(k, -I, I, ...) with K and L set from the book. D() checks legality (if the move is impossible, D() will not return I and k will not change). This prevents “hanging” on an impossible book move.

9. Book Generation (Python + Stockfish)

The book is generated offline on a PC. The Python script uses three stages:

Stage 1: Main Openings (4 positions)

Generates positions after the 4 principal first moves: e2e4, d2d4, c2c4, g1f3. These are positions where black is to move (1 ply deep).

Stage 2: Continuations (~400 positions)

For each of the 4 main openings:

  1. Get top-5 best black responses from Stockfish (depth 12, multipv=5)
  2. For each response, enumerate all white replies (3rd ply)
  3. Save the resulting position (black to move again, 3rd ply)

This yields ~4 × 5 × 20 ≈ 400 positions with deep, strong continuations.

Stage 3: Random Fill (~596 positions)

Fill the remaining slots up to BOOK_SIZE = 1000 with random positions at odd plies (3, 5, 7, 9, 11, 13) — always black to move.

Only odd plies! STM32 plays black. Positions where white is to move (even plies) will never be looked up and waste Flash space. The generator uses depth = random.choice([1, 3, 5, 7, 9, 11, 13]) to ensure this.

Entry Structure in debut_book.c

typedef struct {
    uint32_t key;     /* Zobrist hash (lower 32 bits) */
    uint16_t move;    /* Polyglot move: from(6) | to(6) | promo(3) */
    uint8_t  weight;  /* weight (1 = best move, see note) */
} polyglot_entry_t;
Bug fixed (v7.0): Weight changed from 255 to 1. The working 19-entry reference book used weight = 1, and the binary search in polyglot_find_move returns the first matching entry regardless of weight. Weight = 1 is sufficient and matches the reference.
Bug fixed (v7.0): Entries are now sorted by key via book.sort(key=lambda x: x[0]) before writing. Without sorting, binary search fails silently — it finds nothing even if the key exists.

Example Entries

const polyglot_entry_t debut_book[] = {
    {0x12345678U, 0x0F12U, 1},  /* position → e2e4 */
    {0x9ABCDEF0U, 0x1C30U, 1},  /* position → c7c5 */
    /* ... 998 entries ... */
};
const int debut_book_size = 1000;
Critical: The Zobrist numbers in zobrist_table.h and in the Python script must be identical. If they differ, keys will not match and the book will not work. Use the same zobrist_table.h file for both.

Validation Tool: debug_keys.py

A diagnostic script that loads zobrist_table.h, computes Zobrist keys for 4 known positions (after 1.e4, 1.c4, 1.d4, 1.Nf3) using all 16 combinations of parameters (rank inversion, color mapping, castling, en passant), and finds the combination that matches the known keys from debut_book.c.

Result: invert_rank=False, color_white_is_1=False, include_castling=True, include_ep=True — all 4 keys matched.

10. Full Data Flow (Diagram)

PC (server) STM32 Memory ──────────── ───── ─────── "e2e4X" ───UART──▶ rc_data[] │ ▼ c[0..3] = "e2e4" data_count = 4 │ ▼ K = 'e'-16*'2'+C = 0x34 (e2 in 0x88) L = 'e'-16*'4'+C = 0x54 (e4 in 0x88) │ ▼ GameHistory[GamePtr++] = 256*K + L │ ▼ goto nxt │ ▼ ┌── nxt: side = k (=8, white) │ │ k ^= D(8, -I, I, Q, 1, j++, O, 8, 2) - I ? 0 : 24 │ │ │ ▼ │ D() finds move e2e4, returns I │ k ^= 0 → k = 16 (black) │ side = 8, k = 16 → side != k → move made │ │ │ ▼ │ side == 8 → book_pending = 1 ◀── FLAG │ │ │ ▼ │ Display: mov = "e2e4 w" │ MAX7219: LED on e2→e4 │ ▼ (next while(1) iteration) book_pending == 1 │ ▼ compute_zobrist(k=16) ◀── k=16 (black, after white's move) │ Board b[] holds position after 1.e4 │ ▼ polyglot_find_move(key, &pg_move) │ ├── FOUND ────────────────────────────────┐ │ 'B' in UART │ │ pg_move → polyglot_to_mm → from88,to88│ │ c[] = "c7c5\n" │ │ data_count = -1 │ │ goto skip_wait │ │ │ │ │ ▼ │ │ K = 'c'-16*'7'+C, L = 'c'-16*'5'+C │ │ GameHistory check → goto nxt │ │ │ │ │ ▼ │ │ D(16, -I, I, ...) → finds c7c5 │ │ k ^= 24 → k = 8 (white) │ │ side != k → move made │ │ side == 16 → book_pending NOT set │ │ │ └── NOT FOUND ────────────────────────┐ │ 'D' in UART │ │ UART RX enabled │ │ while(data_count==0) { │ │ waits for enter or '0' │ │ } │ │ enter=1 → srand(TimingRnd) │ │ D(16, -I, I, ...) → normal search │ │ k ^= 24 → k = 8 (white) │ │ side != k → move made │ │

11. Troubleshooting

SymptomCauseSolution
Marker D always (book never triggers)Zobrist numbers in C and Python don't match, or coordinates not invertedVerify zobrist_table.h is shared; check sq88_to_063: must be (7 - (sq>>4))*8 + (sq&7)
Hang after white's moveBook move fails validation (coordinates swapped)Check polyglot_to_mm: must reverse-invert the rank
Error Pe1072 (declaration after label)Variable declaration immediately after nxt: labelMove declarations to global scope or add ; after label
Book triggers on human's moveBook checked unconditionally (no book_pending guard)Ensure book is only called when book_pending == 1
Linker error: undefined debut_bookdebut_book.c not added to IAR projectRight-click project → Add → Add Files
Duplicate definition zobrist_tablezobrist_table.h included in multiple filesInclude only in polyglot.c; use #pragma once or include guard
Book plays same move alwaysSingle entry per position (no randomization)Generate multiple moves per position with different weights
Book not found after sorting fixEntries not sorted by keyAdd book.sort(key=lambda x: x[0]) before writing debut_book.c
Weight overflow or mismatchweight = 255 instead of 1Use weight = 1 as in the reference book
Book has positions white-to-moveEven plies generated (white moves)Use only odd plies: depth = random.choice([1,3,5,7,9,11,13])
Rank inversion mismatch (Python vs C)Python used (7-rank)*8+filePython: use rank*8+file (no inversion); C: use (7-(sq>>4))*8+(sq&7)
Color mapping mismatchPython used white=1, black=0Python: use color = 0 if piece.color else 1 (white=0, black=1)
[SERVER] Bad confirm format in logSTM32 prints board ASCII art to UART, server doesn't recognize itNot an error — server ignores board visualization and waits for mov ... string

12. Memory Consumption

Based on the IAR linker map file micro_Max_ver4_2.map (1000 entries):

Flash (32 KB total on STM32F103C6)

Used: 25,954 B (79.2%)
ComponentAddressSize (hex)Size (bytes)
.intvec (vector table)0x800'00000xEC236
debut_book.o (book data)0x800'00EC0x1F408,000
main.o (code)0x800'202C0x18A06,304
spi.o, gpio.o, uart.o, core.o0x800'38CC–0x800'3E24—1,664
polyglot.o (code)0x800'45EC0x224548
polyglot.o (Zobrist table)0x800'49E00x18686,248
debut_book.o (size var)0x800'64B40x44
Runtime libs + startupvarious—~2,950
Total used—0x647625,762
Free0x800'65620x1A9E6,814

Book footprint: 8,004 bytes (8 KB) for 1000 entries × 7 bytes + 4 bytes size variable. Free space allows ~851 more entries (up to ~1851 total).

RAM (10 KB total on STM32F103C6)

Used: 10,112 B (98.8%)
ComponentAddressSize (hex)Size (bytes)
.data (initialized globals)0x2000'00000x6096
A[U] hash table (main.o)0x2000'00600xC003,072
T[] evaluation table (main.o)0x2000'0C600x40C1,036
GameHistory (main.o)0x2000'106C0xC0192
rand.o state0x2000'11B00x84132
Other BSS (main.o, uart.o)0x2000'1234–0x2000'137F—331
CSTACK (stack)0x2000'13800x14005,120
Total used—0x278010,112
Free0x2000'27800x80128
RAM is critically tight: only 128 bytes free (1.2%). The stack alone uses 5 KB (50% of RAM). Adding new global variables in main.c will overflow. To free RAM: reduce CSTACK to 4 KB (verify recursion depth of micro-Max) or reduce buffer sizes.

Scaling

Book SizeFlash (book)Flash (total firmware)Fits C6 (32KB)?
500 entries~3.5 KB~21 KB✔ Yes
1000 entries~8 KB~26 KB✔ Yes (current)
1851 entries~13 KB~31 KB✔ Yes (max)
2000 entries~14 KB~32 KB✘ No (needs C8, 64KB)

13. Bug History & Fixes

All bugs discovered and fixed during development of the Polyglot book integration:

#SymptomRoot CauseFix
1Book never triggers (marker D always)Rank inversion in Python: used (7-rank)*8+file, but chess lib uses rank 0 = white's back rankChanged to rank*8+file (no inversion in Python)
2Keys don't match even after rank fixColor mapping in Python: used white=1, black=0, but correct is white=0, black=1Changed to color = 0 if piece.color else 1
3Book found for 1st move but not 2ndEntries not sorted by key — binary search requires sorted arrayAdded book.sort(key=lambda x: x[0]) before writing file
4Book found but move rejectedWeight = 255 instead of 1 (as in reference book)Changed to weight = 1
5Book only works for first moveStage 2 generated positions at even plies (white to move) — STM32 never looks these upChanged to odd plies only: random.choice([1,3,5,7,9,11,13])
6Book too large (>2000 entries)Stage 2 used top-5 responses × all white replies × all 20 first movesLimited to 4 main openings (e4, d4, c4, Nf3) × top-5 × all white replies
7Specific openings missing (e.g. after 1.d4)Random fill rarely hits exact opening positionsStage 1 explicitly adds all 4 main openings; Stage 2 adds continuations
8[SERVER] Bad confirm format in logSTM32 prints ASCII board to UART; server doesn't recognize it as protocol messageNot a bug — server ignores board visualization and waits for mov ... string

Validation: debug_keys.py

A diagnostic script that loads zobrist_table.h, computes Zobrist keys for 4 known positions using all 16 combinations of parameters, and finds the correct one. Output:

*** FOUND! ***
  invert_rank = False
  color_white_is_1 = False
  include_castling = True
  include_ep = True

  1.e4 (black):  0x66FF886D (expected 0x66FF886D) ✓
  1.c4 (black):  0x9941BC1A (expected 0x9941BC1A) ✓
  1.d4 (black):  0xD75F281A (expected 0xD75F281A) ✓
  1.Nf3 (black): 0x835FB199 (expected 0x835FB199) ✓

14. Communication Protocol: STM32 ↔ Server

The STM32 communicates with a Python chess server over UART/Wi-Fi (ESP-01). The protocol is text-based.

Message Formats

DirectionMessageMeaning
Server → STM32e2e4XWhite's move (UCI + suffix)
STM32 → ServerGO e2e4 OKConfirmation (may vary)
STM32 → Servermov e2e4 wMove confirmation: mov <uci> <color>
STM32 → Serverrnbqkbnr...ASCII board dump (for visual debug, ignored by server)
Server → STM320XServer finished, expects black's move
STM32 → Servermov e7e5 bBlack's move (from book or engine)
STM32 → ServerB or DMarker: B = book move, D = engine search

Move Lifecycle

Server STM32 Notes ────── ───── ───── "c2c4X" ──────────▶ Parse move Display board (UART) "mov c2c4 w" ──────────▶ Server accepts book_pending = 1 "0X" ──────────▶ compute_zobrist() polyglot_find_move() ├── FOUND: │ "B" ──────────▶ (book marker) │ "mov e7e5 b" ──▶ (black's reply) │ └── NOT FOUND: "D" ──────────▶ (engine marker) D() runs search "mov g8f6 b" ──▶ (engine's reply)
“Bad confirm format” messages in the log are normal. The STM32 prints an ASCII board representation to the UART for visual debugging. The server does not recognize this as a protocol message and logs it as “Bad confirm format”, then waits for the next valid mov ... string. This is not an error — it is expected behavior.