CS 240 Lab 2

Data as Bits

CS 240 Lab 2

Data as Bits

In lab today, you will continue to learn about digital logic, and how bits are used to represent data. You will experiment with Boolean functions and basic gates, including some investigation of binary numbers. Some of the exercises will involve building physical circuits on your protoboard, as we learned to do last week. You will also use a simulator to simulate some circuits.

In the final exercises, you will begin to explore how to think about and solve bit puzzles.

Exercise 1: Binary Circuits

Hexadecimal representation is often used in place of or in conjunction with binary representation when working with binary numbers. This is because hexadecimal is more concise than binary, but is easy to convert to/from binary, since both bases (2 and 16) are powers of two. So, a single digit in hexadecimal is always a 4-digit value in binary, as follows:

Hex Binary
QD QC QB QA
0 0 0 0 0
1 0 0 0 1
2 0 0 1 0
3 0 0 1 1
4 0 1 0 0
5 0 1 0 1
6 0 1 1 0
7 0 1 1 1
8 1 0 0 0
9 1 0 0 1
A 1 0 1 0
B 1 0 1 1
C 1 1 0 0
D 1 1 0 1
E 1 1 1 0
F 1 1 1 1

Note the patterns in this truth table:

  • The least significant bit of the 4-bit value QA toggles every number.
  • The next-least-significant bit QB toggles every other number.
  • The QC toggles every four numbers.
  • The QD toggles every eight numbers.

An IC 7493 is plugged into your workbench at the top right. Examine the following pinout and logic diagram, and the accompanying picture of wires in the board:

A pinout diagram for the 7493 chip, showing the following labels for each pin (clockwise around the diagram from the top-left): Pin 1 is CKB, pin 2 is R0(1), Pin 3 is R0(2), Pin 4 is N.C., Pin 5 is Vcc, Pin 6 is N.C., Pin 7 is N.C. (that’s the left side). On the right, pin 8 is QC, pin 9 is QB, pin 10 is GND, pin 11 is QD, pin 12 is QA, pin 13 is N.C., and pin 14 is CKA. N.C. stands for “not connected.” 

A logic diagram of a 7493 chip with connections. It shows inputs A, B, R1, and R2 on the left of the chip, input Vcc and output GND on the top and bottom respectively, and outputs QA, QB, QC, and QD on the right side of the chip. Each input/output is labeled with a pin number and has connections indicated. Input A is pin 14 and connects to PB1 (a push-button input). Input B is pin 1 and connects around to input 12. Input R1 is pin 2 and connects to an external ground connection. R2 (pin 3) and the GND output (pin 10) also connect to ground. Vcc is pin 5 and connects to +5V. Output QA is pin 12 and connects both to pin 1/input A and to LED indicator #8. QB is pin 9 and connects to LED #7, QC is pin 8 and connects to LED #6, and QD is pin 11 and connects to LED #5. 

A 7493 chip connected to several inputs and outputs. The 7493 is plugged into a protoboard across the middle divide, so that each of its 14 pins are plugged into different rows of the middle section. Connections listed are made by plugging wires into open slots in the same row as the name pin. Ground and 5V rails are available on either edge of the protoboard, so those connections are short. Going around counterclockwise from the top-left pin, pin 1 is connected to pin 12, pin 2 is connected to ground, pin 3 is also connected to ground, pin 5 is connected to +5V, pin 8 is connected to logic indicator #6, pin 9 is connected to logic indicator #7, pin 10 is connected to ground, pin 11 is connected to logic indicator #5, pin 12 is connected to logic indicator #8 (and also back to pin 1, as mentioned already), and pin 14 is connected to a wire that goes out of the picture over to the 1st push-button input. Pins not mentioned are left disconnected. 

Note: “PB1” in the logic diagram refers to one of the “push-button” inputs on your board.

Which wire in the picture represents the Vcc connection?

Example answer: The short red wire connected to the third foot from the bottom on the left side of the chip.

Which wire in the picture represents the GND connection?

Example answer: The short black wire connected to the third foot from the bottom on the right side of the chip.

Which pin does the wire that leaves the edge of the picture connect to? Where is it going?

Example answer: It’s connected to the top-right pin. Since pins are counted counterclockwise around the chip from the top-left and there are 7 pins on each side, that’s pin #14. It’s going out-of-frame because it needs to connect to the push-button input all the way on the bottom-left side of the workbench.

Why are pins 8, 9, 11, and 12 connected to indicators numbered 6, 7, 5, and 8, instead of being connected to LEDs in the same order as the pins are listed?

Example answer: That’s because the logic diagram shows that outputs QA, QB, QC, and QD are connected on pints 12, 9, 8, and 11, i.e., NOT in increasing pin order. We want the LEDs to be in-order according to the output labels, not the pin numbers.

Now, go ahead and wire up the chip on your own board, as shown in the picture / according to the logic diagram. (Check the section on logic diagrams in the slides: you need to map the diagram to the actual physical layout of the chip.)

Turn the power on, and observe the four logic indicators. Press the PB1 button repeatedly, until all of them indicate low voltage.

Now continue pressing PB1, recording the logic indicator outputs (1 for on/high-voltage and 0 for off/low-voltage) in the table to the right until all of the indicators are off again (click on each cell to toggle between 0 and 1).

Do you recognize this pattern? Describe what the 7493 chip does concisely:

Example answer: It’s a 4-bit binary counter that counts up by 1 each time the button is pressed and then released.

Now turn off the power, but don’t disconnect the circuit.

GCorrect answer:

Presses QD QC QB QA
0 0 0 0 0
1 0
1
0
1
0
1
0
1
2 0
1
0
1
0
1
0
1
3 0
1
0
1
0
1
0
1
4 0
1
0
1
0
1
0
1
5 0
1
0
1
0
1
0
1
6 0
1
0
1
0
1
0
1
7 0
1
0
1
0
1
0
1
8 0
1
0
1
0
1
0
1
9 0
1
0
1
0
1
0
1
10 0
1
0
1
0
1
0
1
11 0
1
0
1
0
1
0
1
12 0
1
0
1
0
1
0
1
13 0
1
0
1
0
1
0
1
14 0
1
0
1
0
1
0
1
15 0
1
0
1
0
1
0
1

Correct answer: See above

Exercise 2: Hex Display

Below the 7493 chip on your board is a TIL311 chip. We’ll connect it to the outputs of the 7493 to see how it works.

Follow the logic diagram shown below to connect the TIL311, referring to the logic diagram of the 7493 that we’ve shown above as well, and to the pinout for the TIL311 also shown below. From the 7493 to the TIL311, You’ll need to connect QA to A0, QB to A1, QC to A2, and QD to A3. Then wire up the Vcc, GND, and enable inputs of the TIL311 so that it is activated. The picture on the right shows what this should look like when connected on your board:

A pinout for the TIL 311, showing pins 1-7 down the left-hand side and pins 8-14 up the right-hand side. The pins are labeled: 1: LED supply V-led. 2: Latch data input B. 3: Latch data input A. 4: Left D.P. Cathode. 5: Latch strobe input. 6: No pin. 7: Ground. 8 (at bottom right): Blanking input. 9: No pin. 10: Right D.P. Cathode. 11: No pin. 12: Latch Data Input D. 13: Latch Data input C. 14: Logic Supply Vcc. The middle of the chip has a bunch of small black squares lined up in the pattern of a square 8 (these symbolize the LEDs on the chip which can light up in various patterns to display different digits). 

A logic diagram of a TIL311 chip with connections. It shows inputs A0, A1, A2, and A3 on pins 3, 2, 13, and 12 respectively. These are each connected to something outside the diagram. The Vcc pin on top is pin 14, and connects to +5V; pin 1 also connects there. Pins 5, 8, and 7 connect to ground; pin 7 is labeled GND and pin 5 is labeled “Enable” with a bar above it. There are no outputs in the diagram. 

A picture of both the 7493 and the TIL311 connected on the protoboard. The 7493 sits about 10 rows above the TIL311. The 7493 is connected as shown in the picture above, but with 4 extra wires connecting pins on the 7493 to pins on the TIL311: pin 8 to pin 13, pin 9 to pin 2, pin 11 to pin 12, and pin 12 to pin 3. The other connections from the TIL311 are: pins 1 and 14 to +5V, pins 5, 7, and 8 to ground. Red LEDs built into the clear red face of the TIL311 display the digit 0, and circuit traces internal to the chip can also be seen below the clear face. 

Once you have completed your wiring, power the board back on and record the symbols displayed on the TIL311:

  1. What symbol is displayed when the outputs of the 7493 are all 0? Correct answer: 0
  2. What symbol is displayed when the outputs of the 7493 are 0101? Correct answer: 5
  3. What symbol is displayed when the outputs of the 7493 are 1010? Correct answer: A
  4. What symbol is displayed when the outputs of the 7493 are 1111? Correct answer: F

What do these symbols represent? What is the purpose of the TIL311, and how is it related to what the 7493 does?

Example answer: These are the hexidecimal digits corresponding to the binary inputs, interpreted as a single integer value. The purpose of the TIL311 is to display a 4-digit binary number as a hex digit that humans can easily read. Since the 7493 counts clock pulses as a 4-wire binary signal, the TIL311 works well with it to take in 4 binary signals and display the corresponding number.

Exercise 3: Timing Diagrams

Using a simulator, you can create a timing diagram that shows patterns of high and low voltage over time. Although a truth table is sufficient to describe the behavior of a combinational circuit, for sequential circuits, a timing diagram is needed, since the outputs depend not only on the instantaneous state of the inputs, but on the history of activations over time.

Ti] learn more about LogicWorks and the Falstad simulator, we’ll hook up the same circuit you just built on the workbench in both simulators. Consult the diagrams and instructions below to hook things up:

A LogicWorks circuit centered around a 74_93 chip. The chip has “93” written on it, and has the following connections: CLKA and CLKB are clock inputs on the bottom left. They include circles outside the chip to indicate that they are inverted. Inputs R01 and R02 are on the top left. Outputs QA through QD are on the right. The connections are as follows: R01 and R02 are pins 2 and 3, connected together to ground. CLKA is in 14 connected to a clock. The wire to the clock has a pink label “CK.” CLKB is pin 1 and connects around the bottom of the chip over to the wire coming out of output QA. That output (pin 12) is connected to a binary probe, with a pink QA label on that wire. Outputs QB, QC, and QD are each connected the same way: a labeled wire that connects to a binary probe. They are pins 9, 8, and 11. Each of the binary probes displays ‘X’ as the output. 

A Falstad simulator circuit with a clock component in the middle. It has a clock input (indicated by a triangle), an input ‘R’, and then outputs Q0, Q1, Q2, and Q3. The clock input is connected to a clock (labeled CLK, with a labeled node ‘C’ attached). The ‘R’ input is connected to ground. The Q0 through Q3 outputs are each connected to a numeric logic output, and those connections are each also connected to labeled nodes with names QA, QB, QC, and QD, respectively. 

To build these circuits, follow these directions:

LogicWorks

  1. Search for ‘74_93’ to create the 7493 chip (drag it from the box on the right into the main circuit design area).
    • Note that the Vcc and ground connections are automatic.
  2. Search for ‘clock’ to make a clock component to use for input to CLKA.
  3. Connect R01 and R02 to ground (search “ground”).
  4. Search for “binary” and use “Binary Probe” to hook up the outputs (one probe attached to each ouptut).
  5. Drag a wire from the CLKB input around to the right-hand side and hook it up to the QA output (doing this in two steps is easiest, first dragging down and to the right, and then clicking again and dragging up).
  6. Use the Text tool (‘A’ symbol) to label the connections to the clock and to each binary probe.
    • Make sure the labels turn pink, which should happen when you hit the wire.
    • These should show up in the timing window at the bottom as they are labeled.
  7. Once the circuit is complete, click “clear unknowns” (stomping foot icon). Use the slider near the ‘stop’ button to slow down the simulation speed.
    • Use “clear unknowns” while the simulator is running.

Use the Simulation -> Show Values option to get LogicWorks to display the voltage state of each wire.

Falstad

  1. Use Draw -> Digital Chips -> Counter to create the counter chip.
    • Note the lack of R02 and CLKB. This is simpler than a 7493.
  2. Draw -> Inputs and Sources -> Clock for the clock input.
  3. Draw -> Inputs and Sources -> Ground for the ground connection.
  4. Draw -> Logic Gates, Input and Output -> Logic Output for the indicators.
    • Double-click and select “numeric;” then copy/paste
  5. Draw -> Outputs and Labels -> Labeled Node for the labels.
  6. To display the timing graph, right-click each label in order and select “view in new scope.” Then select Scopes -> Stack All.
    • The labels for each scope won’t be shown, but you can hover over them to see which label they correspond to.

You can use this simulator window to build the Falstad version of the circuit. You’ll have to open LogicWorks separately.

When you’ve built the circuit in both Falstad and LogicWorks, take a look at the signals shown in the timing window. They should match the patterns shown below:

A timing window showing signals CK, QA, QB, QC, and QD. Each signal in order has on- and off-segments that are twice as long as the previous signal, and all of their transitions line up. 

Unlabeled blue lines forming the same pattern as shown above, with one signal going up and down periodically, a second doing the same thing but with up and down periods twice as long, and then third and fourth signals each doubling the length of the ‘on’ and ‘off’ periods. 

How long is each high-voltage-period of the QD signal relative to the high-voltage periods of the clock signal?

Example answer: 16 times as long.

Referring to the first diagram above, imagine that the bottom signal in that diagram (the clock signal which has the shortest pulses) is generated by pressing a button, with each high pulse happening while the button is held down and each low pulse when the button is released. In that scenario, do the QA-QD signals (the four above the clock signal) update when the button is pressed, or when it is released?

When it’s pressed.
When it’s released.

Now look at the second diagram above (the one without numbers along the top axis). In this diagram, the clock signal is the top one. Does it have the same behavior as the LogicWorks diagram you looked at for the previous question, in terms of when things update relative to the clock edges?

Yes, it behaves the same.
No, it’s different.
Explanation: The two simulators do things differently: Falstad’s 7493 equivalent updates on rising edges, while LogicWorks’ version updates on falling edges. We’ll see more about edges in later labs.

Describe how the timing diagrams are related to the truth table for the binary counter:

Example answer: At each time point in the timing diagram, we can see whether each signal is low or high. In the truth table for binary numbers, the QA column flips between 0 and 1 in each row, then the QB flips every other row, the QC every 4 rows and QD every 8 rows. In the timing diagrams, we have the same relationship between signals and clock ticks: the QA signal flips from high to low on the down-tick of each clock pulse, QB flips every other clock pulse, QC every four pulses, and QD every 8.

Exercise 4: Circuit Design

A parity bit is an extra bit of information which is sent when data is transmitted, to check for errors in transmission.

For a given set of bits, the occurrences of bits whose value is 1 is counted. If that count is odd, the parity bit value is set to 1, making the total count of occurrences of 1’s in the whole set (including the parity bit) an even number. If the count of 1’s in a given set of bits is already even, the parity bit value is 0, so in that case the count remains even for the whole set.

Fill in the truth table at the right for P, to produce an even parity bit for inputs ABCD.

An XOR or “exclusive or” gate is useful in a circuit to produce a parity bit. Why?

Example answer: When combined with XOR, high bits will cancel each other out, but will persist when paired with a low bit. So for the 2 bits going into an XOR gate, the result of the XOR will be a 1 if there were an odd number of input bits turned on, and a 0 if there were an even number turned on.

Using either LogicWorks or the Falstad simulator (there’s a window below), design a circuit with 4 inputs whose output P is the parity bit for those four inputs, according to the truth table you just filled out. Use XOR gates. Describe your design:

Example answer: The inputs are grouped into two pairs and fed into two XOR gates. The outputs from these enter a third XOR gate to produce the parity bit. A screenshot from LogicWorks: Four binary inputs feed into two XOR gates, with the top two and bottom two inputs each feeding into one of the two gates. Then the outputs from those two XOR gates feeds into a third XOR gate, the output of which is displayed using a binary probe. In this picture, all inputs are set to 0, so the output is also 0.

Test your design using 4-5 rows of the truth table you just completed.

GCorrect answer:

A B C D P
0 0 0 0 0
1
0 0 0 1 0
1
0 0 1 0 0
1
0 0 1 1 0
1
0 1 0 0 0
1
0 1 0 1 0
1
0 1 1 0 0
1
0 1 1 1 0
1
1 0 0 0 0
1
1 0 0 1 0
1
1 0 1 0 0
1
1 0 1 1 0
1
1 1 0 0 0
1
1 1 0 1 0
1
1 1 1 0 0
1
1 1 1 1 0
1

Correct answer: See above

Here’s a Falstad simulator window if you’d like to use it instead of LogicWorks:

Exercise 5: Bit Puzzles

On the next assignment, you will write some short C programs which implement functions using a limited set and number of C bitwise, logic, and arithmetic operations.

The functions you will write will perform some logical operation. You are given a skeleton for each function, and you will replace the “return” expression with one or more lines of C code that implements the function. Your code must conform to the following style:

int Funct(arg1, arg2, ...) {
    /* brief description of how your implementation works */
    int var1 = Expr1;
    ...
    int varM = ExprM;

    varJ = ExprJ;
    ...
    varN = ExprN;
    return ExprR;
}

Each “Expr” is an expression using ONLY the following:

  1. Integer constants 0 through 255 (0xFF), inclusive. You are not allowed to use big constants such as 0xffffffff.
  2. Function arguments and local variables (no global variables).
  3. Unary integer operations ! ~
  4. Binary integer operations & ^ | + << >>

Some of the problems restrict the set of allowed operators even further.

Each “Expr” may consist of multiple operators. You are not restricted to one operator per line.

You are expressly forbidden to:

  1. Use any control constructs such as if, do, while, for, switch, etc.
  2. Define or use any macros.
  3. Define any additional functions in this file.
  4. Call any functions.
  5. Use any other operations, such as &&, ||, -, or ?:
  6. Use any form of casting.
  7. Use any data type other than int. This implies that you cannot use arrays, structs, or unions.

You may assume that your machine:

  1. Uses 2s complement, 32-bit representations of integers.
  2. Performs right shifts arithmetically.
  3. Has unpredictable behavior when shifting an integer by more than 32 bits.

Note: Integers are represented as 32-bit values in our machines. But, it is cumbersome to write that many digits on paper when you are verifying your results by hand. So, pretend you only have 4-bit representation (it will usually translate to a correct solution for 32-bit numbers, as well).

Here is an example of the type of functions you will write:

/*
 * pow2plus1
 * returns 2ⁿ + 1, where 0 <= n <= 31
 */
int pow2plus1(int n) {
       return (1 << n) + 1;
}

Explain why shifting left is an important part of the solution:

Example answer: We’re not allowed to use a function like pow from math.c, and since n is a variable, without being able to use a loop, we can’t fall back on repeated multiplication to compute 2^n. However, shifting left multiplies by 2, so 2^n can be computed by using a left shift, which is the only option available to us for this problem.

For each of the following values for n, show that 2ⁿ + 1 is equivalent to (1 << n) + 1:

If you prefer, you can do your work on paper.

n = 0 = 0000₂

Example answer: 2^0 + 1 is 1 + 1 or 2.
1 << 0 is 1, and 1 + 1 is also 2.

n = 1 = 0001₂

Example answer: 2^1 is 2, plus 1 is 3.
1 << 1 is 10₂ which is 2 in base 10, plus 1 is 3.

n = 3 = 0011₂

Example answer: 2^3 is 8, plus 1 is 9. 1 << 3 is 1000₂, which is 8 in base 10, plus 1 is also 9.


Now, write a solution for the following function, using the specified rules:

/*
 *   bitOr(int x, int y)     produces    x | y    using only ~ and &
 *   Example: bitOr(6, 5) = 7
 *   Legal ops: ~ &
 *   Max ops: 8
 */
int bitOr(int x, int y) {
  return 2;     // 2 is just a placeholder; replace it
}

Example answer:

/*
 *   bitOr(int x, int y)     produces    x | y    using only ~ and &
 *   Example: bitOr(6, 5) = 7
 *   Legal ops: ~ &
 *   Max ops: 8
 */
int bitOr(int x, int y) {
    return ~(~x&~y);
}

Show that your code works by expressing the following values as 4-bit binary numbers and showing your work:

x = 1, y = 3

Example answer:

x = 0001₂ y = 0011₂

~x = 1110₂ ~y = 1100₂

~x & ~y = 1110₂ & 1100₂ = 1100₂

(x & ~y) = 0011₂ = 3

x|y = 0001₂|0011₂ = 0011₂ = 3

x = 0, y = 5

Example answer:

x = 0000₂ y = 0101₂

~x = 1111₂ ~y = 1010₂

~x & ~y = 1111₂ & 1010₂ = 1010₂

(x & ~y) = 0101₂ = 5

x|y = 0000₂|0101₂ = 0101₂ = 5

x = 3, y = 4

Example answer:

x = 0011₂ y = 0100₂

~x = 1100₂ ~y = 1011₂

~x & ~y = 1100₂ & 1011₂ = 1000₂

(x & ~y) = 0111₂ = 7

x|y = 0011₂|0100₂ = 0111₂ = 7


Given a positive integer, write a function to find if it is a power of two or not.

/*
 * isPower2 - returns 1 if x is a positive value which is power of 2, and
 *     returns 0 otherwise
 *   Examples: isPower2(5) = 0, isPower2(8) = 1, isPower(-1) = 0,
 *     isPower2(0) = 0
 *   Legal ops: ! ~ & ^ | + <<  >>
 *   Max ops: 20
 *   Rating: 4
 */
int isPower2(int x) {
  return 2;     // 2 is just a placeholder; replace it
}

If you were writing a program in Python or Java, how would you detect a power of 2?

Example answer:

First thing I can think of is to use modulus with 2 followed by division in a loop until we end up with either a 1 somewhere as a result of the modulus (so it’s not a power of 2) or the value ends up as 1:

def ip2(x):
    while x > 1:
        if x % 2 != 0:
            return False
        x //= 2
    return True

If you cannot apply this strategy in C using the rules given for this assignment, study some values to see if you recognize any patterns in the numbers which may help you.

Write down the 4-bit values for numbers 0 to 7:

GCorrect answer:

Decimal Binary
0 Correct answer: 0000
1 Correct answer: 0001
2 Correct answer: 0010
3 Correct answer: 0011
4 Correct answer: 0100
5 Correct answer: 0101
6 Correct answer: 0110
7 Correct answer: 0111

Correct answer: See above

Which of these are powers of 2?

Example answer: 0001, 0010, 0100

Do you notice anything significant about the pattern of bits for the numbers that are powers of 2 compared to the other values?

Example answer: They’re the only patterns which include exactly 1. All other patterns have either 0 1’s or more than 1 1.

Experiment with how you can use the allowed operators to detect the powers of 2. You can use:

! logical NOT

~ bitwise complement

& bitwise AND ^ bitwise XOR | bitwise OR + addition

You can also use constant values up to 255 (0xFF) in your expressions.

Because all numbers which are a power of two have only one bit position set, one less than a power of two will always have all the bits lower than that position set and all the bits above that 0.

Here are the powers of 2 with the next smaller number, x + -1, below them:

Expr
x: 0001 0010 0100
x-1: 0000 0001 0011

Experiment with applying each of the allowed bitwise operators to the pairs of numbers x and x-1.

What operator gives you the same result for each pair?

Example answer: & operator gives 0000 for each pair.

Now, for the non-powers of two x, list x + -1 and apply the operator you discovered:

Non-powers of 2

GCorrect answer:

Expr
x: 0011 0101 0110 0111
x-1: 0010 0100 0101 0110
Correct answer: 0010 Correct answer: 0100 Correct answer: 0100 Correct answer: 0110

Correct answer: See above

Do you get the same result by applying the operator to these pairs of values as you did for the powers of 2?

Example answer: Nope, each of these gives a non-zero result when & is applied.

If your answer is no, you have found a way to detect a power of 2!

Write an expression that returns 0 for non-powers of 2 (Note: x-1 can be accomplished with x + ~0):

Example answer: !(x & (x + ~0))

If x = 0, will you get the correct result with this expression?

Example answer: No, because x + ~0 is 0000 + 1111, which is 1111, and 0000 & 1111 is 0, so which when ! is applied becomes 0001, which is wrong, because 0 is NOT a power of 2.

The answer should be no: so, the value 0 must be treated as a special case.

If the number is 0, you should simply return 0.

If the number is stored in x, what expression will return 0 when the number is 0?

Example answer: Just x…

One of the bitwise operators can be used to connect the two expressions so that when x is 0, return 0, otherwise, return the expression which detects a power of two.

Write the full expression:

Example answer:

My first attempt (does work…)

!(x & (x + ~0)) + ~!x + 1

Less complex second term using ^ to combine them:

!(x & (x + ~0)) ^ !x

Will this expression also return 0 for all negative numbers?

Example answer: Nope

If not, add to your expression to also detect a negative value:

Example answer:

This version uses x >> 31 to get just the sign bit of x, and then uses ! to produce a 0 for negative numbers (positive sign bit) and a 1 for positive numbers. By combining that using &, we effectively force the result to be 0 for all negative numbers, but leave the result as-is based on the rest of the expression for positive numbers.

!(x & (x + ~0)) ^ !x & ! (x >> 31)