Шины данных: от отдельных битов к байтам

В прошлой статье мы собрали FullAdder — модуль, который складывает три бита (a, b и перенос) и выдаёт один бит суммы и один бит переноса.

Но давай представим: у нас есть два 8-битных числа, которые нужно сложить. Скажем, 113 (01110001) и 29 (00011101). Как нам это сделать, если у нас есть только одноразрядный FullAdder?

Напрашивается очевидное решение: взять 8 полных сумматоров, соединить их последовательно, подать на каждый соответствующие биты чисел, а перенос с предыдущего подать на вход следующего. И это правильное решение.

Но как это записать в Verilog? Неужели нам придётся объявлять 16 отдельных входов (a0, a1, ..., a7, b0, b1, ..., b7) и 16 промежуточных проводов? Это же 32 имени, 32 строки объявлений, и каждый провод нужно вручную соединить. Для 8-битного сложения это уже муторно, а для 64-битного — практически невозможно.

К счастью, в Verilog есть понятие шины (bus), или вектора (vector).

Что такое шина и зачем она нужна

В реальном цифровом устройстве данные почти никогда не передаются по одному проводу. Когда процессор читает 8-битное значение из памяти, ему нужно 8 проводов, по одному на каждый бит. Эти 8 проводов, проложенные параллельно, и образуют шину данных.

В Verilog шина объявляется с помощью диапазона в квадратных скобках:

input  [7:0] a;   // 8-битный вход a (биты от 7 до 0)
input  [7:0] b;   // 8-битный вход b
output [7:0] sum; // 8-битный выход sum
output       cout; // 1-битный выход переноса

Запись [7:0] читается как «от 7 до 0 включительно». Первое число (7) — это старший бит (MSB, Most Significant Bit). Последнее (0) — младший бит (LSB).

Как обращаться к отдельным битам шины

Вектор — это, по сути, массив проводов. Ты можешь обратиться к любому биту по индексу:

wire [7:0] data;
wire lsb = data[0];  // младший бит (разряд 0)
wire msb = data[7];  // старший бит (разряд 7)

Можно выбрать диапазон битов (part-select):

wire [3:0] low_nibble  = data[3:0];  // младшие 4 бита
wire [3:0] high_nibble = data[7:4];  // старшие 4 бита

Собираем Ripple Carry Adder

Теперь у нас есть инструмент, чтобы собрать 8-битный сумматор. Идея простая: берём 8 экземпляров FullAdder, соединяем их цепочкой, где перенос с каждого предыдущего подаётся на вход следующего. Такая конструкция называется Ripple Carry Adder — «сумматор с пульсирующим переносом».

Почему «пульсирующим»? Потому что перенос бежит по цепочке сумматоров, как «волна» (ripple): сначала младший бит считает, выдаёт перенос, тот бежит к следующему, следующий считает с учётом переноса, выдаёт свой перенос, и так далее до самого старшего бита.

Обрати внимание: у самого младшего сумматора (FA 0) вход cin подключён к 0, потому что переносить из «разряда -1» неоткуда. У самого старшего выход cout — это глобальный перенос, который сигнализирует о переполнении (результат не поместился в 8 бит).

Пишем код: 8x FullAdder вручную

module RippleCarryAdder8(
    input  [7:0] a,
    input  [7:0] b,
    output [7:0] sum,
    output       cout
);
    wire [7:0] c;

    FullAdder fa0 (.a(a[0]), .b(b[0]), .cin(1'b0), .sum(sum[0]), .cout(c[0]));
    FullAdder fa1 (.a(a[1]), .b(b[1]), .cin(c[0]), .sum(sum[1]), .cout(c[1]));
    FullAdder fa2 (.a(a[2]), .b(b[2]), .cin(c[1]), .sum(sum[2]), .cout(c[2]));
    FullAdder fa3 (.a(a[3]), .b(b[3]), .cin(c[2]), .sum(sum[3]), .cout(c[3]));
    FullAdder fa4 (.a(a[4]), .b(b[4]), .cin(c[3]), .sum(sum[4]), .cout(c[4]));
    FullAdder fa5 (.a(a[5]), .b(b[5]), .cin(c[4]), .sum(sum[5]), .cout(c[5]));
    FullAdder fa6 (.a(a[6]), .b(b[6]), .cin(c[5]), .sum(sum[6]), .cout(c[6]));
    FullAdder fa7 (.a(a[7]), .b(b[7]), .cin(c[6]), .sum(sum[7]), .cout(cout));
endmodule

Код длинноват, но он кристально прозрачен: каждый из 8 битов складывается отдельно, переносы передаются по цепочке.

Укрощаем повторение с generate

module RippleCarryAdder8(
    input  [7:0] a,
    input  [7:0] b,
    output [7:0] sum,
    output       cout
);
    wire [8:0] c;
    assign c[0] = 1'b0;

    genvar i;
    generate
        for (i = 0; i < 8; i = i + 1) begin : fa_chain
            FullAdder fa (
                .a(a[i]), .b(b[i]), .cin(c[i]),
                .sum(sum[i]), .cout(c[i+1])
            );
        end
    endgenerate

    assign cout = c[8];
endmodule

generate — это директива времени синтеза, которая позволяет создавать повторяющиеся структуры. Представь цикл for, но не во время выполнения программы, а во время сборки схемы.

Как перенос «пульсирует»

У каждого логического вентиля есть задержка распространения сигнала. Когда a[0] и b[0] меняются, FA0 вычисляет sum[0] и c[1]. Чтобы FA1 начал считать, ему нужно дождаться c[1]. Когда FA1 заканчивает, c[2] бежит к FA2... и так далее, как волна.

Задержка нарастает линейно: для 8-битного сумматора — ~48 вентиль-задержек, для 64-битного — ~384. Это называется критический путь (critical path). Чем длиннее критический путь, тем медленнее может работать процессор.

Именно поэтому реальные процессоры не используют 64-битные Ripple Carry Adder — задержка слишком велика. Вместо этого применяются более хитрые схемы: Carry Lookahead Adder (сумматор с ускоренным переносом).

Ещё пример: конкатенация шин

wire [7:0] a = 8'b10101010;
wire [15:0] word = {a, b}; // 16-битное слово

// Конкатенация: собираем шину из отдельных битов
wire [7:0] d = {a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7]};

Конкатенация {...} — мощный инструмент. Она работает как «сборка» шины из кусочков.

Резюме

1. Шины (векторы) объявляются с диапазоном [7:0] и позволяют работать с группой проводов как с единым целым.

2. К отдельным битам шины можно обращаться по индексу: a[3], a[0].

3. Ripple Carry Adder — это цепочка из FullAdder, где перенос каждого предыдущего сумматора подаётся на вход следующего.

4. Для повторяющихся структур используется generate — «цикл во время синтеза».

5. Задержка распространения сигнала (ripple delay) — главная проблема последовательного переноса.

На уровне 3.4 тебе предстоит собрать 8-битный Ripple Carry Adder, используя 8 экземпляров FullAdder (который ты собрал на уровне 3.3) и шинную разводку от двух 8-битных входов к 8-битному выходу. Попробуй оба способа: ручной и с generate.