Шины данных: от отдельных битов к байтам
В прошлой статье мы собрали 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.