Part II · Chapter 6
Modbus ASCII
Modbus ASCII is RTU's slower but more legible cousin. It carries exactly the same PDU, but spells every byte out as two readable text characters, so you can almost read a frame straight off a terminal.
By the end of this chapter you can
- Turn any byte into the two ASCII characters that carry it.
- Recognize an ASCII frame by its colon and CR/LF delimiters.
- Compute an LRC by hand.
- Convert a request between RTU and ASCII, and say when you'd meet ASCII.
ASCII is rare on new equipment, but it still turns up on legacy systems and on links where RTU's strict timing can't be guaranteed, so it's worth a short chapter.
Bytes as text
The defining idea is simple: every byte travels as two ASCII characters that spell its value in hexadecimal. The byte 0x03 isn't sent as the byte 0x03; it's sent as the printable characters 0 and 3. The byte 0xF8 goes as F and 8. Since each byte becomes two characters, an ASCII frame is about twice as long on the wire as the same RTU frame, which is the main reason ASCII is slower.
The payoff for that expansion is readability. A frame made entirely of printable characters passes cleanly through terminals, modems, and links that were never designed to carry arbitrary binary data and might choke on it. And a person watching the line sees recognizable text rather than an opaque byte stream, which made ASCII genuinely useful for debugging in the days of simpler tools.
Framing by delimiters
RTU marks the edges of a frame with silence, a timed gap of 3.5 character times. ASCII takes a different and in some ways friendlier approach: delimiter characters. Every frame begins with a colon (:) and ends with a carriage return and line feed (written CR/LF). A receiver watches for a colon to know a frame is starting, and for CR/LF to know it's over.
Delimiter framing is ASCII's quiet advantage. Because the edges of a frame are real characters, not precise timing, ASCII tolerates pauses and irregular delays that would break an RTU frame. The receiver doesn't care how long the gaps between characters are; it only cares about the colon and the closing newline. That forgiveness lets ASCII survive on slow or jittery links, including software and operating-system paths where RTU's tight timing is hard to guarantee.
The LRC check
ASCII protects its frames with a simpler check than RTU's CRC: a one-byte LRC, or longitudinal redundancy check. Add up the binary values of the message bytes (address, function code, and data, but not the colon or the CR/LF), keep only the lowest byte of the sum, and take its two's complement (negate it, modulo 256). That byte is appended, as two hex characters like everything else, just before the CR.
def lrc(data):
return (-sum(data)) & 0xFF
fields = bytes([0x01, 0x03, 0x00, 0x00, 0x00, 0x04])
frame = ":" + fields.hex().upper() + f"{lrc(fields):02X}" + "\r\n"
print(repr(frame))
':010300000004F8\r\n'
That first function is the entire algorithm. For our running example (address 01, function 03, start 00 00, quantity 00 04), the bytes sum to 0x08, and the two's complement of 0x08 is 0xF8. So the LRC is 0xF8, riding in the frame as the characters F and 8.
The LRC is weaker than the CRC at catching some error patterns (two errors that cancel in the sum slip straight through, for example), which is part of why RTU became the preferred serial form. For ASCII's gentler use cases, it's adequate.
The same message, two ways
Seeing one complete request in both serial forms makes the relationship vivid. Our example (read four holding registers from unit 1, starting at address 0) looks like this:
| Form | On the wire |
|---|---|
| Modbus RTU | 01 03 00 00 00 04 44 09 (8 bytes, binary) |
| Modbus ASCII | :010300000004F8 + CR/LF (17 characters, text) |
The same fields appear in the same order in both: 01, 03, 0000, 0004. RTU sends them as raw bytes and closes with a two-byte CRC; ASCII spells them out as hex characters inside a colon and a newline and closes with a one-byte LRC. The PDU is identical; only the encoding and the wrapper differ. It's the Chapter 4 lesson again, now between two serial dialects instead of between serial and Ethernet.
RTU ⇄ ASCII converter
RTU bytes → ASCII frame
ASCII frame → bytes
When you will meet it
In practice, expect RTU and reach for it by default; ASCII is the exception. You'll meet it mainly on older equipment that predates the move to RTU, or when a device manual specifically calls for it. Some installations chose ASCII deliberately because their path (a radio link, a modem, a chain of converters) couldn't preserve RTU's timing but could reliably carry plain text.
Configuration is the same as RTU plus one choice: select ASCII instead of RTU as the transmission mode, and still match baud rate, data bits, parity, and stop bits at both ends. One difference to expect: because every character is plain ASCII text, ASCII mode normally uses 7 data bits (the specification's default is 7-E-1), where RTU always uses 8.
With both serial forms in hand, we've covered Modbus on wires. The remaining transport carries Modbus across the networks that run modern facilities: Modbus TCP, which swaps the serial address for a small header and drops the checksum entirely.
Check your understanding
1. How does Modbus ASCII send the byte 0x3A?
Every byte becomes two characters spelling its hexadecimal value. (A lone 0x3A would also be a colon, the start character, which is one reason ASCII spells bytes out.)
2. How does an ASCII receiver find the edges of a frame?
ASCII uses delimiter characters, so pauses between characters don't matter. Timing-based framing is RTU's method.
3. The message bytes of a frame sum to 0x08. What is the LRC?
The LRC is the two's complement of the low byte of the sum: 0x100 − 0x08 = 0xF8. (0xF7 is the one's complement, a common slip.)
4. Why might an installation deliberately choose ASCII over RTU?
ASCII is slower and its check is weaker, but delimiter framing survives the pauses and jitter that break RTU frames.