Browse Source

Suggesting a third exercise for bit manipulation

Alexander Sisco 6 months ago
parent
commit
3faad6e17b
2 changed files with 475 additions and 0 deletions
  1. 32 0
      build.zig
  2. 443 0
      exercises/110_bit_manipulation3.zig

+ 32 - 0
build.zig

@@ -1208,6 +1208,38 @@ const exercises = [_]Exercise{
         \\Max difference (new fn): 0.014
         \\Max difference (new fn): 0.014
         ,
         ,
     },
     },
+    .{ .main_file = "110_bit_manipulation3.zig", .output = 
+    \\Set pins with OR on PORTB
+    \\-------------------------
+    \\  1001 // (initial state of PORTB)
+    \\| 0100 // (bitmask)
+    \\= 1101
+    \\
+    \\  1001 // (reset state)
+    \\| 0100 // (bitmask)
+    \\= 1101
+    \\
+    \\Clear pins with AND and NOT on PORTB
+    \\------------------------------------
+    \\  1110 // (initial state of PORTB)
+    \\& 1011 // (bitmask)
+    \\= 1010
+    \\
+    \\  0111 // (reset state)
+    \\& 1110 // (bitmask)
+    \\= 0110
+    \\
+    \\
+    \\Toggle pins with XOR on PORTB
+    \\-----------------------------
+    \\  1100 // (initial state of PORTB)
+    \\^ 0101 // (bitmask)
+    \\= 1001
+    \\
+    \\  1100 // (initial state of PORTB)
+    \\^ 0011 // (bitmask)
+    \\= 1111
+    },
     .{
     .{
         .main_file = "999_the_end.zig",
         .main_file = "999_the_end.zig",
         .output =
         .output =

+ 443 - 0
exercises/110_bit_manipulation3.zig

@@ -0,0 +1,443 @@
+// ----------------------------------------------------------------------------
+// Setting, Clearing, and Toggling Bits
+// ----------------------------------------------------------------------------
+//
+// Another exciting thing about Zig is its suitability for embedded
+// programming. Your Zig code doesn't have to remain on your laptop. You can
+// also deploy your code to microcontrollers! This means you can write Zig to
+// drive your next robot or greenhouse climate control system! Ready to enter
+// the exciting world of embedded programming? This exercise is designed to
+// give you a taste of what it's like to control registers in a
+// microcontroller. Let's get started!
+//
+// A common activity in microcontroller programming is setting and clearing
+// bits on input and output pins. This lets you control LEDs, sensors, motors
+// and more! In a previous exercise (097_bit_manipulation.zig) you learned how
+// to swap two bytes using the ^ (XOR - exclusive or) operator. In this
+// exercise, we'll take a closer look at bit manipulation and how we can write
+// code that sets and clears specific bits as we would if we were programming
+// the pins on a real microcontroller. Included at the end of this exercise are
+// some helper functions that demonstrate how we might make our code a little
+// more readable.
+//
+// Below is a pinout diagram for the famous ATmega328 AVR microcontroller used
+// as the primary microchip on popular microcontroller platforms like the
+// Arduino UNO.
+//
+//  ============ PINOUT DIAGRAM FOR ATMEGA328 MICROCONTROLLER ============
+//                                _____ _____
+//                               |     U     |
+//                 (RESET) PC6 --|  1     28 |-- PC5
+//                         PD0 --|  2     27 |-- PC4
+//                         PD1 --|  3     26 |-- PC3
+//                         PD2 --|  4     25 |-- PC2
+//                         PD3 --|  5     24 |-- PC1
+//                         PD4 --|  6     23 |-- PC0
+//                         VCC --|  7     22 |-- GND
+//                         GND --|  8     21 |-- AREF
+//                     |-- PB6 --|  9     20 |-- AVCC
+//                     |-- PB7 --| 10     19 |-- PB5 --|
+//                     |   PD5 --| 11     18 |-- PB4 --|
+//                     |   PD6 --| 12     17 |-- PB3 --|
+//                     |   PD7 --| 13     16 |-- PB2 --|
+//                     |-- PB0 --| 14     15 |-- PB1 --|
+//                     |         |___________|         |
+//                     \_______________________________/
+//                                    |
+//                                  PORTB
+//
+// Drawing inspiration from this diagram, we'll use the pins for PORTB as our
+// mental model for this exercise in bit manipulation. It should be noted that
+// in the following examples we are using ordinary variables, one of which we
+// have named PORTB, to simulate modifying the bits of real hardware registers.
+// But in actual microcontroller code, PORTB would be defined something like
+// this:
+//          pub const PORTB = @as(*volatile u8, @ptrFromInt(0x25));
+//
+// This lets the compiler know not to make any optimizations to PORTB so that
+// the IO pins are properly mapped to our code.
+//
+// NOTE : To keep things simple, the following examples are given using type
+// u4, so applying the output to PORTB would only affect the lower four pins
+// PB0..PB3. Of course, there is nothing to prevent you from swapping the u4
+// with a u8 so you can control all 8 of PORTB's IO pins.
+
+const std = @import("std");
+const print = std.debug.print;
+const testing = std.testing;
+
+pub fn main() !void {
+    var PORTB: u4 = 0b0000; // only 4 bits wide for simplicity
+    //
+    // ------------------------------------------------------------------------
+    // 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", .{});
+
+    PORTB = 0b1001; // reset PORTB
+    print("  {b:0>4} // (initial state of PORTB)\n", .{PORTB});
+    print("| {b:0>4} // (bitmask)\n", .{0b0100});
+    PORTB = PORTB ??? (1 << 2); // What's missing here?
+    checkAnswer(0b1101, PORTB);
+
+    newline();
+
+    PORTB = 0b1001; // reset PORTB
+    print("  {b:0>4} // (reset state)\n", .{PORTB});
+    print("| {b:0>4} // (bitmask)\n", .{0b0100});
+    PORTB ??? (1 << 2); // What's missing here?
+    checkAnswer(0b1101, PORTB);
+
+    newline();
+
+    // ------------------------------------------------------------------------
+    // 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
+    //     we can write this as:
+    //          ~(1 << 2) -> 1011
+    //
+    //     But if we try to compile ~(1 << 2), 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", .{});
+
+    PORTB = 0b1110; // reset PORTB
+    print("  {b:0>4} // (initial state of PORTB)\n", .{PORTB});
+    print("& {b:0>4} // (bitmask)\n", .{0b1011});
+    PORTB = PORTB & ???@as(u4, 1 << 2); // What character is missing here?
+    checkAnswer(0b1010, PORTB);
+
+    newline();
+
+    PORTB = 0b0111; // reset PORTB
+    print("  {b:0>4} // (reset state)\n", .{PORTB});
+    print("& {b:0>4} // (bitmask)\n", .{0b1110});
+    PORTB &= ~(1 << 0); // What's missing here?
+    checkAnswer(0b0110, PORTB);
+
+    newline();
+    newline();
+
+
+    //
+    // ------------------------------------------------------------------------
+    // 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.
+
+    print("Toggle pins with XOR on PORTB\n", .{});
+    print("-----------------------------\n", .{});
+    PORTB = 0b1100;
+    print("  {b:0>4} // (initial state of PORTB)\n", .{PORTB});
+    print("^ {b:0>4} // (bitmask)\n", .{0b0101});
+    PORTB ^= (1 << 1) | (1 << 0); // What's wrong here?
+    checkAnswer(0b1001, PORTB);
+
+    newline();
+
+    PORTB = 0b1100;
+    print("  {b:0>4} // (initial state of PORTB)\n", .{PORTB});
+    print("^ {b:0>4} // (bitmask)\n", .{0b0011});
+    PORTB ^= (1 << 1) & (1 << 0); // What's wrong here?
+    checkAnswer(0b1111, PORTB);
+
+    // 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();
+}
+
+// ----------------------------------------------------------------------------
+// Here are some helper functions for manipulating bits
+// ----------------------------------------------------------------------------
+
+// Functions for setting, clearing, and toggling a single bit
+fn setBit(comptime T: type, byte: T, comptime bit_pos: T) !T {
+    return byte | (1 << bit_pos);
+}
+
+test "setBit" {
+    try testing.expectEqual(setBit(u8, 0b0000_0000, 3), 0b0000_1000);
+}
+
+fn clearBit(comptime T: type, byte: T, comptime bit_pos: T) T {
+    return byte & ~@as(T, (1 << bit_pos));
+}
+
+test "clearBit" {
+    try testing.expectEqual(clearBit(u8, 0b1111_1111, 0), 0b1111_1110);
+}
+
+fn toggleBit(comptime T: type, byte: T, comptime bit_pos: T) T {
+    return byte ^ (1 << bit_pos);
+}
+
+test "toggleBit" {
+    var byte = toggleBit(u8, 0b0000_0000, 0);
+    try testing.expectEqual(byte, 0b0000_0001);
+    byte = toggleBit(u8, byte, 0);
+    try testing.expectEqual(byte, 0b0000_0000);
+}
+
+// ----------------------------------------------------------------------------
+// Some additional functions for setting, clearing, and toggling multiple bits
+// at once with a tuple because, hey, why not?
+// ----------------------------------------------------------------------------
+//
+
+fn createBitmask(comptime T: type, comptime bits: anytype) !T {
+    comptime var bitmask: T = 0;
+    inline for (bits) |bit| {
+        if (bit >= @bitSizeOf(T)) return error.BitPosTooLarge;
+        if (bit < 0) return error.BitPosTooSmall;
+
+        bitmask |= (1 << bit);
+    }
+    return bitmask;
+}
+
+test "creating bitmasks from a tuple" {
+    try testing.expectEqual(createBitmask(u8, .{0}), 0b0000_0001);
+    try testing.expectEqual(createBitmask(u8, .{1}), 0b0000_0010);
+    try testing.expectEqual(createBitmask(u8, .{2}), 0b0000_0100);
+    try testing.expectEqual(createBitmask(u8, .{3}), 0b0000_1000);
+    //
+    try testing.expectEqual(createBitmask(u8, .{ 0, 4 }), 0b0001_0001);
+    try testing.expectEqual(createBitmask(u8, .{ 1, 5 }), 0b0010_0010);
+    try testing.expectEqual(createBitmask(u8, .{ 2, 6 }), 0b0100_0100);
+    try testing.expectEqual(createBitmask(u8, .{ 3, 7 }), 0b1000_1000);
+
+    try testing.expectError(error.BitPosTooLarge, createBitmask(u4, .{4}));
+}
+
+fn setBits(byte: u8, bits: anytype) !u8 {
+    const bitmask = try createBitmask(u8, bits);
+    return byte | bitmask;
+}
+
+test "setBits" {
+    try testing.expectEqual(setBits(0b0000_0000, .{0}), 0b0000_0001);
+    try testing.expectEqual(setBits(0b0000_0000, .{7}), 0b1000_0000);
+
+    try testing.expectEqual(setBits(0b0000_0000, .{ 0, 1, 2, 3, 4, 5, 6, 7 }), 0b1111_1111);
+    try testing.expectEqual(setBits(0b1111_1111, .{ 0, 1, 2, 3, 4, 5, 6, 7 }), 0b1111_1111);
+
+    try testing.expectEqual(setBits(0b0000_0000, .{ 2, 3, 4, 5 }), 0b0011_1100);
+
+    try testing.expectError(error.BitPosTooLarge, setBits(0b1111_1111, .{8}));
+    try testing.expectError(error.BitPosTooSmall, setBits(0b1111_1111, .{-1}));
+}
+
+fn clearBits(comptime byte: u8, comptime bits: anytype) !u8 {
+    const bitmask: u8 = try createBitmask(u8, bits);
+    return byte & ~@as(u8, bitmask);
+}
+
+test "clearBits" {
+    try testing.expectEqual(clearBits(0b1111_1111, .{0}), 0b1111_1110);
+    try testing.expectEqual(clearBits(0b1111_1111, .{7}), 0b0111_1111);
+
+    try testing.expectEqual(clearBits(0b1111_1111, .{ 0, 1, 2, 3, 4, 5, 6, 7 }), 0b000_0000);
+    try testing.expectEqual(clearBits(0b0000_0000, .{ 0, 1, 2, 3, 4, 5, 6, 7 }), 0b000_0000);
+
+    try testing.expectEqual(clearBits(0b1111_1111, .{ 0, 1, 6, 7 }), 0b0011_1100);
+
+    try testing.expectError(error.BitPosTooLarge, clearBits(0b1111_1111, .{8}));
+    try testing.expectError(error.BitPosTooSmall, clearBits(0b1111_1111, .{-1}));
+}
+
+fn toggleBits(comptime byte: u8, comptime bits: anytype) !u8 {
+    const bitmask = try createBitmask(u8, bits);
+    return byte ^ bitmask;
+}
+
+test "toggleBits" {
+    try testing.expectEqual(toggleBits(0b0000_0000, .{0}), 0b0000_0001);
+    try testing.expectEqual(toggleBits(0b0000_0000, .{7}), 0b1000_0000);
+
+    try testing.expectEqual(toggleBits(0b1111_1111, .{ 0, 1, 2, 3, 4, 5, 6, 7 }), 0b000_0000);
+    try testing.expectEqual(toggleBits(0b0000_0000, .{ 0, 1, 2, 3, 4, 5, 6, 7 }), 0b1111_1111);
+
+    try testing.expectEqual(toggleBits(0b0000_1111, .{ 0, 1, 2, 3, 4, 5, 6, 7 }), 0b1111_0000);
+    try testing.expectEqual(toggleBits(0b0000_1111, .{ 0, 1, 2, 3 }), 0b0000_0000);
+
+    try testing.expectEqual(toggleBits(0b0000_0000, .{ 0, 2, 4, 6 }), 0b0101_0101);
+
+    try testing.expectError(error.BitPosTooLarge, toggleBits(0b1111_1111, .{8}));
+    try testing.expectError(error.BitPosTooSmall, toggleBits(0b1111_1111, .{-1}));
+}
+
+// ----------------------------------------------------------------------------
+// Utility functions
+// ----------------------------------------------------------------------------
+
+fn newline() void {
+    print("\n", .{});
+}
+
+fn checkAnswer(expected: u4, answer: u4) void {
+    if (expected != answer) {
+        print("*************************************************************\n", .{});
+        print("= {b:0>4} <- INCORRECT! THE EXPECTED OUTPUT IS {b:0>4}\n", .{ answer, expected });
+        print("*************************************************************\n", .{});
+    } else {
+        print("= {b:0>4}", .{answer});
+    }
+    newline();
+}
+