moved explanatory content below the broken code in

main so that the exercise functions more like a quiz
This commit is contained in:
Alexander Sisco 2025-02-11 11:56:57 -08:00
parent 20596bc290
commit a7cd808bb8

View file

@ -68,40 +68,31 @@ const testing = std.testing;
pub fn main() !void {
var PORTB: u4 = 0b0000; // only 4 bits wide for simplicity
// The LCD display on our robot is not behaving as expected. In order to
// get it functioning properly, we must initialize it by sending the
// correct sequence of half-bytes to PORTB's lower four pins.
//
// Let's first take a look at toggling bits.
// See if you can solve the following problems to get the lcd working and
// reveal the message our robot has stored in his EEPROM.
//
// ------------------------------------------------------------------------
// Toggling bits with XOR:
// ------------------------------------------------------------------------
// XOR stands for "exclusive or". We can toggle bits with the ^ (XOR)
// bitwise operator, like so:
// .--. .--.
// | | | |
// +--------------------------+
// | +----------------------+ |
// | | | |
// | | XXXXXXXX XXXXXXXX | | <-- LCD
// | | | |
// | +----------------------+ |
// | _________ |
// | |_|_|_|_|_| |
// | |
// +--------------------------+
// | |
//
//
// In order to output a 1, the logic of an XOR operation requires that the
// two input bits are of different values. Therefore, 0 ^ 1 and 1 ^ 0 will
// both yield a 1 but 0 ^ 0 and 1 ^ 1 will output 0. XOR's unique behavior
// of outputing a 0 when both inputs are 1s is what makes it different from
// the OR operator; it also gives us the ability to toggle bits by putting
// 1s into our bitmask.
//
// - 1s in our bitmask operand, can be thought of as causing the
// corresponding bits in the other operand to flip to the opposite value.
// - 0s cause no change.
//
// The 0s in our bitmask preserve these values
// -XOR op- ---expanded--- in the output.
// _______________/
// / /
// 0110 1 1 0 0
// ^ 1111 0 1 0 1 (bitmask)
// ------ - - - -
// = 1001 1 0 0 1 <- This bit was already cleared.
// \_______\
// \
// We can think of these bits having flipped
// because of the presence of 1s in those columns
// of our bitmask.
// The last two problems throw you a bit of a curve ball. Try solving them
// on your own. If you need help, scroll to the bottom to see some in depth
// explanations on toggling, setting, and clearing bits in Zig.
print("Toggle pins with XOR on PORTB\n", .{});
print("-----------------------------\n", .{});
@ -121,49 +112,6 @@ pub fn main() !void {
newline();
// Now let's take a look at setting bits with the | operator.
//
// ------------------------------------------------------------------------
// Setting bits with OR:
// ------------------------------------------------------------------------
// We can set bits on PORTB with the | (OR) operator, like so:
//
// var PORTB: u4 = 0b1001;
// PORTB = PORTB | 0b0010;
// print("PORTB: {b:0>4}\n", .{PORTB}); // output: 1011
//
// -OR op- ---expanded---
// _ Set only this bit.
// /
// 1001 1 0 0 1
// | 0010 0 0 1 0 (bit mask)
// ------ - - - -
// = 1011 1 0 1 1
// \___\_______\
// \
// These bits remain untouched because OR-ing with
// a 0 effects no change.
//
// ------------------------------------------------------------------------
// To create a bit mask like 0b0010 used above:
//
// 1. First, shift the value 1 over one place with the bitwise << (shift
// left) operator as indicated below:
// 1 << 0 -> 0001
// 1 << 1 -> 0010 <-- Shift 1 one place to the left
// 1 << 2 -> 0100
// 1 << 3 -> 1000
//
// This allows us to rewrite the above code like this:
//
// var PORTB: u4 = 0b1001;
// PORTB = PORTB | (1 << 1);
// print("PORTB: {b:0>4}\n", .{PORTB}); // output: 1011
//
// Finally, as in the C language, Zig allows us to use the |= operator, so
// we can rewrite our code again in an even more compact and idiomatic
// form: PORTB |= (1 << 1)
print("Set pins with OR on PORTB\n", .{});
print("-------------------------\n", .{});
@ -183,86 +131,6 @@ pub fn main() !void {
newline();
// So now we've covered how to toggle and set bits. What about clearing
// them? Well, this is where Zig throws us a curve ball. Don't worry we'll
// go through it step by step.
// ------------------------------------------------------------------------
// Clearing bits with AND and NOT:
// ------------------------------------------------------------------------
// We can clear bits with the & (AND) bitwise operator, like so:
// PORTB = 0b1110; // reset PORTB
// PORTB = PORTB & 0b1011;
// print("PORTB: {b:0>4}\n", .{PORTB}); // output -> 1010
//
// - 0s clear bits when used in conjuction with a bitwise AND.
// - 1s do nothing, thus preserving the original bits.
//
// -AND op- ---expanded---
// __________ Clear only this bit.
// /
// 1110 1 1 1 0
// & 1011 1 0 1 1 (bit mask)
// ------ - - - -
// = 1010 1 0 1 0 <- This bit was already cleared.
// \_______\
// \
// These bits remain untouched because AND-ing with a
// 1 preserves the original bit value whether 0 or 1.
//
// ------------------------------------------------------------------------
// We can use the ~ (NOT) operator to easily create a bit mask like 1011:
//
// 1. First, shift the value 1 over two places with the bit-wise << (shift
// left) operator as indicated below:
// 1 << 0 -> 0001
// 1 << 1 -> 0010
// 1 << 2 -> 0100 <- The 1 has been shifted two places to the left
// 1 << 3 -> 1000
//
// 2. The second step in creating our bit mask is to invert the bits
// ~0100 -> 1011
// in C we would write this as:
// ~(1 << 2) -> 1011
//
// But if we try to compile ~(1 << 2) in Zig, we'll get an error:
// unable to perform binary not operation on type 'comptime_int'
//
// Before Zig can invert our bits, it needs to know the number of
// bits it's being asked to invert.
//
// We do this with the @as (cast as) built-in like this:
// @as(u4, 1 << 2) -> 0100
//
// Finally, we can invert our new mask by placing the NOT ~ operator
// before our expression, like this:
// ~@as(u4, 1 << 2) -> 1011
//
// If you are offput by the fact that you can't simply invert bits like
// you can in languages such as C without casting to a particular size
// of integer, you're not alone. However, this is actually another
// instance where Zig is really helpful because it protects you from
// difficult to debug integer overflow bugs that can have you tearing
// your hair out. In the interest of keeping things sane, Zig requires
// you simply to tell it the size of number you are inverting. In the
// words of Andrew Kelley, "If you want to invert the bits of an
// integer, zig has to know how many bits there are."
//
// For more insight into the Zig team's position on why the language
// takes the approach it does with the ~ operator, take a look at
// Andrew's comments on the following github issue:
// https://github.com/ziglang/zig/issues/1382#issuecomment-414459529
//
// Whew, so after all that what we end up with is:
// PORTB = PORTB & ~@as(u4, 1 << 2);
//
// We can shorten this with the &= combined AND and assignment operator,
// which applies the AND operator on PORTB and then reassigns PORTB. Here's
// what that looks like:
// PORTB &= ~@as(u4, 1 << 2);
//
print("Clear pins with AND and NOT on PORTB\n", .{});
print("------------------------------------\n", .{});
@ -283,37 +151,222 @@ pub fn main() !void {
newline();
newline();
// ------------------------------------------------------------------------
// Conclusion
// ------------------------------------------------------------------------
//
// While the examples in this exercise have used only 4-bit wide variables,
// working with 8 bits is no different. Here's a an example where we set
// every other bit beginning with the two's place:
// var PORTD: u8 = 0b0000_0000;
// print("PORTD: {b:0>8}\n", .{PORTD});
// PORTD |= (1 << 1);
// PORTD = setBit(u8, PORTD, 3);
// PORTD |= (1 << 5) | (1 << 7);
// print("PORTD: {b:0>8} // set every other bit\n", .{PORTD});
// PORTD = ~PORTD;
// print("PORTD: {b:0>8} // bits flipped with NOT (~)\n", .{PORTD});
// newline();
//
// // Here we clear every other bit beginning with the two's place.
//
// PORTD = 0b1111_1111;
// print("PORTD: {b:0>8}\n", .{PORTD});
// PORTD &= ~@as(u8, 1 << 1);
// PORTD = clearBit(u8, PORTD, 3);
// PORTD &= ~@as(u8, (1 << 5) | (1 << 7));
// print("PORTD: {b:0>8} // clear every other bit\n", .{PORTD});
// PORTD = ~PORTD;
// print("PORTD: {b:0>8} // bits flipped with NOT (~)\n", .{PORTD});
// newline();
}
// ************************************************************************
// IN-DEPTH EXPLANATIONS BELOW
// ************************************************************************
// ------------------------------------------------------------------------
// Toggling bits with XOR:
// ------------------------------------------------------------------------
// XOR stands for "exclusive or". We can toggle bits with the ^ (XOR)
// bitwise operator, like so:
//
//
// In order to output a 1, the logic of an XOR operation requires that the
// two input bits are of different values. Therefore, 0 ^ 1 and 1 ^ 0 will
// both yield a 1 but 0 ^ 0 and 1 ^ 1 will output 0. XOR's unique behavior
// of outputing a 0 when both inputs are 1s is what makes it different from
// the OR operator; it also gives us the ability to toggle bits by putting
// 1s into our bitmask.
//
// - 1s in our bitmask operand, can be thought of as causing the
// corresponding bits in the other operand to flip to the opposite value.
// - 0s cause no change.
//
// The 0s in our bitmask preserve these values
// -XOR op- ---expanded--- in the output.
// _______________/
// / /
// 0110 1 1 0 0
// ^ 1111 0 1 0 1 (bitmask)
// ------ - - - -
// = 1001 1 0 0 1 <- This bit was already cleared.
// \_______\
// \
// We can think of these bits having flipped
// because of the presence of 1s in those columns
// of our bitmask.
//
// Now let's take a look at setting bits with the | operator.
//
// ------------------------------------------------------------------------
// Setting bits with OR:
// ------------------------------------------------------------------------
// We can set bits on PORTB with the | (OR) operator, like so:
//
// var PORTB: u4 = 0b1001;
// PORTB = PORTB | 0b0010;
// print("PORTB: {b:0>4}\n", .{PORTB}); // output: 1011
//
// -OR op- ---expanded---
// _ Set only this bit.
// /
// 1001 1 0 0 1
// | 0010 0 0 1 0 (bit mask)
// ------ - - - -
// = 1011 1 0 1 1
// \___\_______\
// \
// These bits remain untouched because OR-ing with
// a 0 effects no change.
//
// ------------------------------------------------------------------------
// To create a bit mask like 0b0010 used above:
//
// 1. First, shift the value 1 over one place with the bitwise << (shift
// left) operator as indicated below:
// 1 << 0 -> 0001
// 1 << 1 -> 0010 <-- Shift 1 one place to the left
// 1 << 2 -> 0100
// 1 << 3 -> 1000
//
// This allows us to rewrite the above code like this:
//
// var PORTB: u4 = 0b1001;
// PORTB = PORTB | (1 << 1);
// print("PORTB: {b:0>4}\n", .{PORTB}); // output: 1011
//
// Finally, as in the C language, Zig allows us to use the |= operator, so
// we can rewrite our code again in an even more compact and idiomatic
// form: PORTB |= (1 << 1)
// So now we've covered how to toggle and set bits. What about clearing
// them? Well, this is where Zig throws us a curve ball. Don't worry we'll
// go through it step by step.
// ------------------------------------------------------------------------
// Clearing bits with AND and NOT:
// ------------------------------------------------------------------------
// We can clear bits with the & (AND) bitwise operator, like so:
// PORTB = 0b1110; // reset PORTB
// PORTB = PORTB & 0b1011;
// print("PORTB: {b:0>4}\n", .{PORTB}); // output -> 1010
//
// - 0s clear bits when used in conjuction with a bitwise AND.
// - 1s do nothing, thus preserving the original bits.
//
// -AND op- ---expanded---
// __________ Clear only this bit.
// /
// 1110 1 1 1 0
// & 1011 1 0 1 1 (bit mask)
// ------ - - - -
// = 1010 1 0 1 0 <- This bit was already cleared.
// \_______\
// \
// These bits remain untouched because AND-ing with a
// 1 preserves the original bit value whether 0 or 1.
//
// ------------------------------------------------------------------------
// We can use the ~ (NOT) operator to easily create a bit mask like 1011:
//
// 1. First, shift the value 1 over two places with the bit-wise << (shift
// left) operator as indicated below:
// 1 << 0 -> 0001
// 1 << 1 -> 0010
// 1 << 2 -> 0100 <- The 1 has been shifted two places to the left
// 1 << 3 -> 1000
//
// 2. The second step in creating our bit mask is to invert the bits
// ~0100 -> 1011
// in C we would write this as:
// ~(1 << 2) -> 1011
//
// But if we try to compile ~(1 << 2) in Zig, we'll get an error:
// unable to perform binary not operation on type 'comptime_int'
//
// Before Zig can invert our bits, it needs to know the number of
// bits it's being asked to invert.
//
// We do this with the @as (cast as) built-in like this:
// @as(u4, 1 << 2) -> 0100
//
// Finally, we can invert our new mask by placing the NOT ~ operator
// before our expression, like this:
// ~@as(u4, 1 << 2) -> 1011
//
// If you are offput by the fact that you can't simply invert bits like
// you can in languages such as C without casting to a particular size
// of integer, you're not alone. However, this is actually another
// instance where Zig is really helpful because it protects you from
// difficult to debug integer overflow bugs that can have you tearing
// your hair out. In the interest of keeping things sane, Zig requires
// you simply to tell it the size of number you are inverting. In the
// words of Andrew Kelley, "If you want to invert the bits of an
// integer, zig has to know how many bits there are."
//
// For more insight into the Zig team's position on why the language
// takes the approach it does with the ~ operator, take a look at
// Andrew's comments on the following github issue:
// https://github.com/ziglang/zig/issues/1382#issuecomment-414459529
//
// Whew, so after all that what we end up with is:
// PORTB = PORTB & ~@as(u4, 1 << 2);
//
// We can shorten this with the &= combined AND and assignment operator,
// which applies the AND operator on PORTB and then reassigns PORTB. Here's
// what that looks like:
// PORTB &= ~@as(u4, 1 << 2);
//
// ------------------------------------------------------------------------
// Conclusion
// ------------------------------------------------------------------------
//
// While the examples in this quiz have used only 4-bit wide variables,
// working with 8 bits is no different. Here's a an example where we set
// every other bit beginning with the two's place:
// var PORTD: u8 = 0b0000_0000;
// print("PORTD: {b:0>8}\n", .{PORTD});
// PORTD |= (1 << 1);
// PORTD = setBit(u8, PORTD, 3);
// PORTD |= (1 << 5) | (1 << 7);
// print("PORTD: {b:0>8} // set every other bit\n", .{PORTD});
// PORTD = ~PORTD;
// print("PORTD: {b:0>8} // bits flipped with NOT (~)\n", .{PORTD});
// newline();
//
// // Here we clear every other bit beginning with the two's place.
//
// PORTD = 0b1111_1111;
// print("PORTD: {b:0>8}\n", .{PORTD});
// PORTD &= ~@as(u8, 1 << 1);
// PORTD = clearBit(u8, PORTD, 3);
// PORTD &= ~@as(u8, (1 << 5) | (1 << 7));
// print("PORTD: {b:0>8} // clear every other bit\n", .{PORTD});
// PORTD = ~PORTD;
// print("PORTD: {b:0>8} // bits flipped with NOT (~)\n", .{PORTD});
// newline();
// ----------------------------------------------------------------------------
// Here are some helper functions for manipulating bits
// ----------------------------------------------------------------------------