Day 1 - Calorie Counting
Okay everyone it’s advent of code season, we’re back with another round of Christmas themed programming puzzles, this time helping out some elves on a jungle journey. Let’s get that star fruit. I’ve decided to work on today’s problem in SystemVerilog because… idk… YOLO? Nah, SV is good. And by good, I mean awful, but occasionally a fun exercise; something something thinking outside of the box. I’m not going to preemptively self own by committing to doing the whole calendar in SV, but we’ll see where the spirit of the season takes me.
So, what do we have?
Yadda yadda yadda jungle, yadda yadda supplies, traveling by foot…
One important consideration is food - in particular, the number of Calories each Elf is carrying (your puzzle input).
Mmmmmmm, elf snacks.
The Elves take turns writing down the number of Calories contained by the various meals, snacks, rations, etc. that they’ve brought with them, one item per line.
Seems reasonable.
Each Elf separates their own inventory from the previous Elf’s inventory (if any) by a blank line.
Oh boy, here we go.
For example, suppose the Elves finish writing their items’ Calories and end up with the following list:
1000
2000
3000
4000
5000
6000
7000
8000
9000
10000
This list represents the Calories of the food carried by five Elves:
The first Elf is carrying food with 1000, 2000, and 3000 Calories, a total of 6000 Calories.
The second Elf is carrying one food item with 4000 Calories.
The third Elf is carrying food with 5000 and 6000 Calories, a total of 11000 Calories.
The fourth Elf is carrying food with 7000, 8000, and 9000 Calories, a total of 24000 Calories.
The fifth Elf is carrying one food item with 10000 Calories.
Okay cool, read the input file, split on double newlines ‘\n\n’ to get the inventory for a particular elf, split those inventories on newlines again to get the individual items. Solid. This is a straightforward enough ask for a normalish programming language. Doing this in SystemVerilog is, shall we say, less than ideal. So instead of doing that we’re going to… well, let’s come back to that… For now let’s finish figuring out what the heck it is we need to do with these elf snacks.
In case the Elves get hungry and need extra snacks, they need to know which Elf to ask: they’d like to know how many Calories are being carried by the Elf carrying the most Calories. In the example above, this is 24000 (carried by the fourth Elf).
Find the Elf carrying the most Calories. How many total Calories is that Elf carrying?
Sounds good, find the elf with the best goodies and take their snacks return the sum of calories of those snacks. Cool, so we need to sum, or accumulate, the number of calories of the snacks that each elf is holding, and then compare the total calories each elf is holding to find the maximum.
In pseudocode this would be something like:
max_elf_snack_cals = 0
for each elf:
if sum(elf's snack calories) > max_elf_snack_cals then
max_elf_snack_cals = sum(elf's snack calories)
So how does this translate to verilog?
Well, to begin we’ll need something to add up the snacks. In old school verilog this would be a reg
, in SV we have logic
32 bits to be precise, so:
logic[31:0] calories_sum = 0;
Great, so now we actually have to sum those calories. We’ll do this in an always
block with a vld
input signal to indicate that the value on another input holding the current snack calories is legit and that we want to add it to our running total.
logic[31:0] calories_sum = 0;
always_ff @ (posedge clk) begin
if (food_vld) begin
calories_sum <= calories_sum + food_calories;
end
end
Hmm, but we need to clear that when we’re done summing up one elf’s snacks and it’s time to move on to the next elf. We could do a nested if… but those get kinda gross. How bout a case?
logic[31:0] calories_sum = 0;
always_ff @ (posedge clk) begin
case ({store_sum, food_vld})
0: calories_sum <= calories_sum; // do nothing / keep the same value
1: calories_sum <= calories_sum + food_calories;
2: calories_sum <= 0;
3: calories_sum <= 0; // Requested both accumulate and clear, error
endcase
end
Nice, but what the heck is this case doing? Why are store_sum
and food_vld
in braces, why are you counting from 0 to 3? Where are my elf snacks? I’m hungry.
Okay (here’s my two bits) so I’m treating {store_sum, food_vld}
as a two bit signal, with food_vld
as the low bit, and store_sum
the high. The numbers enumerate the possible values that two bit signal can take, and what should be done in each of those cases. This says:
If food_vld is hot, we add the new calories to our running sum. If we’re storing the sum we clear out our current sum (on the next clock edge). If we try to do both at the same time, I’ve elected to clear the sum, and if neither is requested, keep the same value. Looks pretty good. What if something really crazy happens though? Or what about when we start up? Maybe we need some way to uh, reset, to a good known state.
logic[31:0] calories_sum = 0;
always_ff @ (posedge clk) begin
if (rst) begin
calories_sum <= 0;
end else begin
case ({store_sum, food_vld})
0: calories_sum <= calories_sum; // do nothing / keep the same value
1: calories_sum <= calories_sum + food_calories;
2: calories_sum <= 0;
3: calories_sum <= 0; // Requested both accumulate and clear, error
endcase
end
end
That’s better. At every positive/rising edge of our input clock signal, if there’s a valid food_calories number on our input, add it to our running sum. When we decide to store that sum we clear the running sum. Sweet, we can sum up the values. But where exactly are these values coming from? We’re gonna need some inputs. Now that you mention it, this little code snippet is kind of floating in the ether. Lets put it in a module.
module top
(
input logic clk,
input logic rst,
input logic [31:0] food_calories,
input logic food_vld,
input logic store_sum
);
logic[31:0] calories_sum = 0;
always_ff @ (posedge clk) begin
if (rst) begin
calories_sum <= 0;
end else begin
case ({store_sum, food_vld})
0: calories_sum <= calories_sum; // do nothing / keep the same value
1: calories_sum <= calories_sum + food_calories;
2: calories_sum <= 0;
3: calories_sum <= 0; // Requested both accumulate and clear, error
endcase
end
end
endmodule
Looking good, making sense. Although, right now our store_sum
is more of a clear_sum
, so perhaps we should actually store the calories_sum
somewhere. Let’s add another logic
and another always
block.
logic[31:0] stored_calories = 0;
always_ff @ (posedge clk) begin
if (rst) begin
stored_calories <= 0;
end else begin
if (store_sum) begin
stored_calories <= calories_sum;
end
end
end
Wait… we’re forgetting something. We’re supposed to store the maximum of the calorie sums. Okay, easy enough.
//logic[31:0] stored_calories = 0;
logic[31:0] calories_max = 0;
always_ff @ (posedge clk) begin
if (rst) begin
calories_max <= 0;
end else begin
if (store_sum) begin
calories_max <= (calories_max > calories_sum) ? calories_max : calories_sum;
end
end
end
Boy do I love a ternary conditional. If our max value is greater than our current sum, keep it; otherwise, overwrite it with the current sum. That’s just about it for part one. We just need to take our calories_max and be able to present it to the outside world, so let’s add some logic to hook it up to an output.
input logic read_max;
output logic max_vld;
output logic [31:0] max_calories_sum;
initial begin
max_vld = 0;
max_calories_sum = 0;
end
always_ff @ (posedge clk) begin
if (rst) begin
max_vld <= 0;
max_calories_sum <= 0;
end else if (read_max) begin
max_vld <= 1;
max_calories_sum <= calories_max;
end
end
Not too shabby. Hmm… There is something a little off though. If we did it this way, then after read_max is raised, we set max_vld
high, set max_calories_sum
to our calories_max
and then we leave it like that. That seems problematic. What if we wanted to check the maximum somewhere in the middle while we’re still processing input. If we did that and then kept feeding in new food values and storing sums our max_vld
would stay high, and our max_calories_sum
would be the same as when we requested it, and wouldn’t account for any of our new input, which would be bad. Let’s not do that.
always_ff @ (posedge clk) begin
//if (rst) begin
if (rst || food_vld) begin
max_vld <= 0;
max_calories_sum <= 0;
end else if (read_max) begin
max_vld <= 1;
max_calories_sum <= calories_max;
end
end
Now we clear our output max signals as soon as we get a new food vld input. This should be fine. We could also do this as soon as we get a store_sum request, and this is a place where API / protocol decisions would come into play, but I think what we have is good for now.
Here’s everything we have so far. I’ve adjusted it a bit to put the new inputs and outputs up at the top.
module top
(
input logic clk,
input logic rst,
input logic [31:0] food_calories,
input logic food_vld,
input logic store_sum,
input logic read_max,
output logic max_vld,
output logic [31:0] max_calories_sum
);
logic[31:0] calories_sum = 0;
always_ff @ (posedge clk) begin
if (rst) begin
calories_sum <= 0;
end else begin
case ({store_sum, food_vld})
0: calories_sum <= calories_sum; // do nothing / keep the same value
1: calories_sum <= calories_sum + food_calories;
2: calories_sum <= 0;
3: calories_sum <= 0; // Requested both accumulate and clear, error
endcase
end
end
logic[31:0] calories_max = 0;
always_ff @ (posedge clk) begin
if (rst) begin
calories_max <= 0;
end else begin
if (store_sum) begin
calories_max <= (calories_max > calories_sum) ? calories_max : calories_sum;
end
end
end
initial begin
max_vld = 0;
max_calories_sum = 0;
end
always_ff @ (posedge clk) begin
if (rst || food_vld) begin
max_vld <= 0;
max_calories_sum <= 0;
end else if (read_max) begin
max_vld <= 1;
max_calories_sum <= calories_max;
end
end
endmodule
Nice, we have a 100% correct, first go, definitely gonna work solution.
Right……
So uh, how do we drive this thing?
Find out next time on. Parallel Elves