Binary-coded decimal (BCD) — a system where each decimal digit is represented by four binary bits — was the dominant method for encoding numbers in early computing, persists in modern hardware timekeeping chips, and explains why some numbers in embedded systems look oddly constrained to values like 9, 19, 29 rather than values like 15, 31, or 255
The previous articles on this site covered binary/octal/decimal/hex basics, negative numbers and overflow, bit manipulation and bitmasks, IEEE 754 floating point, three everyday hex formats, and reading hex for debugging. This article addresses number encoding systems beyond standard binary — specifically BCD, Gray code, and one's complement, the historical reasons they exist, and the specific modern contexts where they still appear.
Binary-Coded Decimal: why "09" in binary isn't just 9
Standard binary encodes numbers by their mathematical value: 9 = 1001₂, 10 = 1010₂, 15 = 1111₂, 16 = 10000₂.
Binary-Coded Decimal (BCD) encodes each decimal digit separately:
- Each decimal digit (0-9) is encoded as a 4-bit group
- 9 in BCD =
1001(same as binary 9) - 10 in BCD =
0001 0000(one, zero — two separate 4-bit groups) - 99 in BCD =
1001 1001(nine, nine) - 256 in BCD =
0010 0101 0110(two, five, six — three 4-bit groups)
Why the "odd" constraint: BCD groups can only hold values 0-9. The patterns 1010 through 1111 (10-15 in binary) are "illegal" BCD — they never appear. This is why devices using BCD have limits that look decimal rather than binary: a BCD byte (two 4-bit digits) holds 00-99, not 0-255.
Where BCD still appears in modern hardware
Real-Time Clock (RTC) chips — the hardware clocks that keep time in computers, microcontrollers, embedded systems, and IoT devices — almost universally use BCD encoding:
The DS1307 and DS3231 (popular RTC chips): store seconds, minutes, hours, day, date, month, and year all in BCD format. When you read the "seconds" register, you get a BCD-encoded value:
Register value: 0x59 (binary: 0101 1001)
BCD interpretation: 5 (upper nibble), 9 (lower nibble) → 59 seconds
Binary value: 89 (decimal) — wrong if interpreted as standard binary
The conversion requirement: software reading from an RTC chip must decode BCD:
// BCD to decimal conversion
uint8_t bcd_to_dec(uint8_t bcd) {
return (bcd / 16 * 10) + (bcd % 16);
}
// or equivalently:
uint8_t bcd_to_dec(uint8_t bcd) {
return ((bcd >> 4) * 10) + (bcd & 0x0F);
}
Why RTCs use BCD: RTC chips were designed when displaying time on 7-segment displays was the primary output. 7-segment display controllers natively decode BCD to segment patterns — each digit can drive one display digit directly. The entire pipeline from RTC chip to display required no binary-to-decimal conversion, simplifying circuit design in an era when every component counted.
BCD in financial calculations: avoiding floating-point errors
Financial calculations — currency amounts, tax calculations, accounting — can't afford the floating-point rounding errors discussed in the IEEE 754 article. One approach is BCD arithmetic (also called packed decimal):
IBM's EBCDIC and packed decimal: IBM mainframes used BCD (called "packed decimal") for financial arithmetic for decades. The System/360 instruction set has dedicated BCD arithmetic instructions (Pack, Unpack, Add Packed, Multiply Packed) that operate on packed decimal representations.
Modern language equivalents:
- Java:
BigDecimalclass - Python:
decimalmodule withDecimaltype - C#:
decimaltype (128-bit, not BCD but avoids floating-point issues)
The BCD approach in modern embedded finance: some embedded systems (ATMs, point-of-sale terminals, card readers) still use BCD arithmetic for currency amounts to guarantee exact decimal representation.
Gray code: binary that changes one bit at a time
Gray code (also called "reflected binary code") is a binary encoding where consecutive integers differ by exactly one bit:
| Decimal | Binary | Gray code |
|---|---|---|
| 0 | 000 | 000 |
| 1 | 001 | 001 |
| 2 | 010 | 011 |
| 3 | 011 | 010 |
| 4 | 100 | 110 |
| 5 | 101 | 111 |
| 6 | 110 | 101 |
| 7 | 111 | 100 |
Why one-bit-change matters — the rotary encoder: a rotary encoder measures angular position. If the encoder uses standard binary and is positioned exactly at the transition from 3 (011) to 4 (100), all three bits change simultaneously. During the physical transition, the bits may not change simultaneously — an intermediate reading of 111 (7) or 101 (5) could briefly occur, producing a large erroneous position reading.
With Gray code: 3 is 010, 4 is 110 — only the most significant bit changes. No intermediate invalid states are possible during the transition. This is why virtually all rotary encoders and angular position sensors use Gray code.
Conversion between Gray code and binary:
// Binary to Gray code
unsigned gray = binary ^ (binary >> 1);
// Gray code to binary
unsigned binary = gray;
while (binary >>= 1) gray ^= binary;
// (iterative version; also done with leading bit propagation)
One's complement: the predecessor to two's complement
One's complement was an earlier method for representing negative numbers in binary:
- Positive numbers: standard binary
- Negative numbers: invert all bits (NOT operation)
+5 in 8-bit one's complement: 0000 0101
−5 in 8-bit one's complement: 1111 1010 (all bits inverted)
The problem with one's complement: it has two representations of zero:
- +0 =
0000 0000 - −0 =
1111 1111
Having two zeros complicates arithmetic and comparison. Two's complement (the modern standard) solves this by representing negative numbers as NOT(n) + 1, producing a single zero representation and simpler arithmetic hardware.
Where one's complement still appears: one's complement checksums are used in the TCP and UDP header checksum algorithms (RFC 793 and RFC 768). The checksum is computed by summing 16-bit words in one's complement arithmetic (with carry wrap-around) and taking the one's complement of the result. This was chosen because the one's complement sum is self-inverse — verifying a checksum requires only summing all words and checking for all-ones result.
How to use the Number Base Converter on sadiqbd.com
- For BCD debugging: when reading values from RTC chips, sensors, or legacy hardware that uses BCD, convert each 4-bit nibble independently — don't treat the whole byte as a standard hex value
- For Gray code: the converter handles binary ↔ decimal ↔ hex conversion; Gray code conversion requires the XOR-based formula shown above before or after using the tool
- For embedded system work: when you see a hex value that seems wrong (like 0x59 meaning 59 but reading as 89 in decimal), BCD encoding is the most common culprit — the tool shows the decimal value of the hex, which lets you spot the BCD interpretation difference
Frequently Asked Questions
Why does Python's datetime module sometimes produce values that look like they might be BCD-related when working with timestamps?
Python's datetime module uses standard binary internally — the BCD connection comes from the hardware level, not Python. When you read from an RTC chip using Python (via I2C with smbus or similar), the raw bytes are BCD-encoded. If you forget to convert BCD to decimal before creating a datetime object, you'll get the wrong time — for example, 0x59 (89 decimal) for seconds instead of 59. The issue is at the hardware read layer, not in Python's datetime. The fix: apply bcd_to_dec() conversion to each byte before constructing the datetime object. Libraries like adafruit-circuitpython-ds3231 handle this conversion automatically; lower-level I2C reads require manual BCD conversion.
Is the Number Base Converter free? Yes — completely free, no sign-up required.
Try the Number Base Converter free at sadiqbd.com — convert between binary, octal, decimal, and hexadecimal instantly.