ASCII, Unicode và UTF-8: đếm byte hay đếm ký tự?

Khoảng 45 phútMức 1/3Bản nháp: một số khẳng định chưa đối chiếu spec

Sau bài này bạn sẽ

  • Phân biệt giá trị số, mã ASCII và Unicode code point
  • Tính số byte của các chuỗi UTF-8 cụ thể
  • Nhận ra giới hạn của so byte và độ dài string

Một ký tự nhìn thấy chưa chắc là một byte

Ký tự “A” thường chiếm một byte trong UTF-8. Chữ “ộ” dạng dựng sẵn chiếm ba byte; một biểu tượng như 😀 chiếm bốn byte. Nếu giao diện ghi “độ dài”, cần hỏi đang đếm byte, code point hay ký tự người đọc nhìn thấy.

Các bộ đếm độ dài dùng để cấp phát buffer hoặc truyền dữ liệu phải theo đơn vị của giao thức. Không lấy số ký tự trên màn hình làm số byte cần gửi.

ASCII là một miền mã nhỏ

ASCII định nghĩa 128 mã từ 0 tới 127: chữ Latin cơ bản, chữ số, dấu và các ký tự điều khiển. Ví dụ A có mã hex 41, ký tự 0 có mã 30; code 0 là NUL, không phải ký tự số 0.

Đối tượng Byte hex trong ASCII/UTF-8 Ý nghĩa
Ký tự A 41 Chữ hoa
Ký tự 0 30 Chữ số viết ra
Ký tự xuống dòng LF 0A Ký tự điều khiển
Số nguyên không dấu 10 trong một byte 0A Dữ liệu số, chỉ thành LF khi diễn giải như văn bản

Một byte có 256 mẫu, nhưng ASCII cơ bản chỉ dùng nửa miền ấy. Không có một “extended ASCII” duy nhất cho mọi hệ thống; các code page khác nhau có thể dùng nửa còn lại khác nhau.

Code point khác cách lưu trong bộ nhớ

Unicode gán code point cho các đối tượng ký tự. Cách viết U+0041 chỉ code point của A; nó chưa quy định mọi hệ thống phải dùng bao nhiêu byte để lưu.

UTF-8 và UTF-16 là các cách mã hóa chuỗi code point thành code unit. UTF-8 dùng các code unit một byte; một Unicode scalar hợp lệ dùng từ một tới bốn byte. Miền surrogate không phải scalar để mã hóa trực tiếp như một ký tự UTF-8 hợp lệ.

Font và cách dựng chữ quyết định hình hiển thị. Một hình có thể ghép từ nhiều code point; một code point cũng không bảo đảm luôn có glyph trên máy người dùng.

Encoding Độ rộng một code unit Số code unit cho một scalar hợp lệ
UTF-8 8 bit 1 tới 4
UTF-16 16 bit 1 hoặc 2; scalar ngoài BMP cần surrogate pair
UTF-32 32 bit 1

Scalar là code point trong miền Unicode trừ các code point surrogate. Một surrogate đơn lẻ không phải một scalar để đưa thẳng vào UTF-8. Khi lưu UTF-16/UTF-32 thành byte, còn phải chốt byte order; số code unit không tự xác định thứ tự byte trên đường truyền.

Glyph là hình do font dựng; grapheme cluster là cụm dùng để gần với một ký tự người dùng cảm nhận. Một cụm có thể gồm chữ nền với nhiều dấu. Không dùng độ dài code unit để mặc định số glyph hoặc grapheme cluster.

Đọc vài chuỗi byte cụ thể

Văn bản/cách tạo Code point Byte UTF-8 Số byte
A U+0041 41 1
ộ dựng sẵn U+1ED9 E1 BB 99 3
o + dấu chấm dưới + dấu mũ U+006F U+0323 U+0302 6F CC A3 CC 82 5
😀 U+1F600 F0 9F 98 80 4

Hai hàng tiếng Việt có thể được dựng giống nhau nhưng byte không bằng nhau. Hàng ghép gồm ba code point; đếm byte, code point và cụm ký tự người dùng cảm nhận sẽ cho kết quả khác.

Trong UTF-8 hợp lệ, ASCII giữ nguyên byte từ 00 tới 7F. Byte bắt đầu và các byte tiếp nối của chuỗi nhiều byte có vai trò khác nhau; cắt buffer tùy ý ở một ranh giới byte có thể làm đứt một chuỗi.

Bằng nhau theo tiêu chí nào?

So byte là tiêu chí rõ cho payload nhị phân, hash hoặc một số định danh đã được chuẩn hóa theo hợp đồng. Với văn bản người dùng, hai cách ghép code point có thể cần so theo tiêu chí khác.

Normalization như NFC là một bước xử lý Unicode có quy tắc, không phải thao tác xóa dấu hay chuyển mọi văn bản về ASCII. Cần chốt yêu cầu rồi dùng thư viện phù hợp; không thay các chuỗi byte trong giao thức mà chưa biết có được phép chuẩn hóa không.

Ví dụ của bài chỉ chỉ ra hai chuỗi cụ thể khác byte. Nó không triển khai normalization, so sánh ngôn ngữ, đếm grapheme hay xác nhận toàn bộ chuỗi Unicode đều hợp lệ.

String trong ví dụ HDL là chuỗi byte

Ví dụ dùng literal UTF-8 trong source rồi kiểm độ dài và từng byte mà toolchain giữ lại. Kết quả cho “ộ” dựng sẵn là 3, không phải 1.

Khi test UART gửi văn bản, scoreboard phải biết dữ liệu là byte raw hay UTF-8. Một FIFO rộng tám bit chỉ giữ các byte; nó không tự biết byte nào là đầu hay cuối một ký tự.

Chủ đề UART chỉ là bối cảnh về đơn vị dữ liệu; bài này không triển khai hoặc xác minh protocol. Khi cần xử lý NUL, byte không hợp lệ hoặc I/O binary, phải đọc quy tắc API cụ thể; ví dụ không có các trường hợp ấy.

Các nhầm lẫn gây hỏng dữ liệu

  • Đồng nhất mã ký tự số 0 với byte có giá trị 0.
  • Dùng số hình nhìn thấy để tính dung lượng buffer.
  • Cho rằng hai chuỗi trông giống nhau thì byte phải giống.
  • Cắt một chuỗi nhiều byte ở vị trí bất kỳ rồi mong văn bản vẫn hợp lệ.
  • Cho rằng đổi font là đổi encoding.
  • Dùng string API để xử lý dữ liệu nhị phân mà chưa kiểm cách API xử lý NUL.

Khi debug dữ liệu đi qua giao diện, ghi đơn vị ở từng biên: số byte, số code point hoặc số mục dữ liệu. Một log hex thường giúp thấy chỗ sai rõ hơn ảnh chụp chữ.

Đếm đúng đơn vị

  1. Vì sao ký tự 7 có byte 37 chứ không phải 07 trong ASCII?
  2. Ghép A và ộ dựng sẵn cần bao nhiêu byte UTF-8? Có bao nhiêu code point?
  3. Hai chuỗi tiếng Việt trong bảng có cùng số code point không? Cùng số byte không?
  4. Một buffer chỉ còn hai byte có giữ được trọn chữ ộ dựng sẵn không?
  5. Khi so hai tên người dùng, cần làm rõ những tiêu chí gì trước khi chọn so byte hay normalization?

Kiểm tra

Tự kiểm tra: ASCII, Unicode và UTF-8: đếm byte hay đếm ký tự?

  1. Ký tự 0 trong ASCII có mã nào?
  2. Chữ ộ dựng sẵn U+1ED9 trong bảng cần mấy byte UTF-8?
  3. Hai cách viết tiếng Việt trong bảng có nhất thiết bằng nhau theo byte không?
  4. Buffer còn hai byte có giữ trọn U+1ED9 theo UTF-8 không?
  5. Ví dụ kiểm từng byte của bốn literal đã xác nhận NFC hay đếm grapheme chưa?

Bằng chứng kiểm chứng

Expected tính trước chạy: ENCODING CHECK strings=4 ascii=1 viet=3 decomposed=5 emoji=4 bytes=13 byte_equal=0 time_ns=4.

Chuỗi UTF-8: byte, điểm mã và hình thức hiển thị

Kỳ vọng tính trước chạy: ENCODING CHECK strings=4 ascii=1 viet=3 decomposed=5 emoji=4 bytes=13 byte_equal=0 time_ns=4

tb.sv testbench
`timescale 1ns/1ps// Expected BEFORE execution: 4 UTF-8 source strings, lengths1/3/5/4, total13.// Composed and explicitly decomposed Vietnamese text are different byte strings.module tb_a1_encoding;  string words[4]='{"A","ộ","ộ","😀"};  int expected_length[4]='{1,3,5,4};  int offsets[4]='{0,1,4,9};  byte unsigned expected_octet[13]='{8'h41,8'he1,8'hbb,8'h99,8'h6f,8'hcc,8'ha3,8'hcc,8'h82,8'hf0,8'h9f,8'h98,8'h80};  byte unsigned octet;  int strings_seen=0,bytes_seen=0;  initial begin    for(int i=0;i<4;i++)begin      #1;strings_seen++;      if(words[i].len()!=expected_length[i])$fatal(1,"encoding length mismatch index=%0d got=%0d",i,words[i].len());      for(int j=0;j<expected_length[i];j++)begin        octet=words[i].getc(j);bytes_seen++;        if(octet!==expected_octet[offsets[i]+j])$fatal(1,"encoding byte mismatch string=%0d byte=%0d",i,j);      end    end    if(strings_seen!=4||bytes_seen!=13||words[1]==words[2]||$time!=4)$fatal(1,"encoding experiment contract");    $display("ENCODING CHECK strings=4 ascii=1 viet=3 decomposed=5 emoji=4 bytes=13 byte_equal=0 time_ns=%0d",$time);    $finish;  endendmodule

Đã kiểm chứng thế nào

  • Phân tích và elaborate theo LRM (pyslang 12.1.0)
  • Mô phỏng, exit 0 và output khớp kỳ vọng tính tay (Verilator 5.050, 2-state)

Output phải khớp: ENCODING CHECK strings=4 ascii=1 viet=3 decomposed=5 emoji=4 bytes=13 byte_equal=0 time_ns=4

Verilator 5.050 two-state, finite known inputs only; no X/Z or all-width conformance claim.

Four UTF-8 source literals without NUL; byte length/getc checked under this toolchain. Not a UTF-8 validator, Unicode normalization or grapheme segmentation implementation.

Các kiểm tra chạy lúc xây dựng nội dung (2026-11-07), không chạy trong trình duyệt. Kỳ vọng được tính tay trước khi chạy.

Bốn literal là A, ộ dựng sẵn, chuỗi o + U+0323 + U+0302 và emoji. Tham chiếu chứa 13 byte hex viết trước chạy; kiểm từng byte cùng độ dài và sự khác byte của hai chuỗi tiếng Việt. Parser/simulation đạt trên toolchain đã khóa, không claim mọi simulator giữ source encoding giống nhau.

Lượt đầu phát hiện lỗi tạo literal ghép thành DEộ: độ dài vẫn là 5 nhưng byte đầu sai, nên phép kiểm từng byte đã bắt được. Sửa literal về chuỗi code point dự định rồi chạy lại, giữ nguyên 13 byte tham chiếu.

Chưa kiểm UTF-8 không hợp lệ, NUL, normalization hay grapheme segmentation. Quy tắc string SystemVerilog và Unicode đầy đủ vẫn chờ đối chiếu chuẩn tương ứng.

[1] [2] [3] [4] [5]

Nguồn và mức độ đối chiếu

  1. Character Encoding Systems — bản chụp resources.zip, What is ASCII Code?; ASCII Code Examples; ASCII Character Ranges; Limitations of ASCII; What is Unicode?; Unicode Encoding Formats; UTF-8: The Universal Standard; Common Beginner Mistakes. Trích dẫn; nguồn đã đọc các mục liên quan.
  2. Thí nghiệm nguyên bản A1.encoding, Four literal UTF-8 byte sequences, thirteen expected octets and distinct composed/decomposed bytes. Trích dẫn; nguồn đã đọc toàn văn.
  3. IEEE 1800-2023 SystemVerilog LRM, String literal source encoding, len/getc byte access and string comparison — chưa đối chiếu LRM. Chưa đối chiếu với văn bản nguồn (chưa mở).
  4. RFC 3629: UTF-8 — chờ đối chiếu, Valid UTF-8 scalar encoding and byte sequences — chưa đối chiếu RFC. Chưa đối chiếu với văn bản nguồn (chưa mở).
  5. Unicode UAX 15: Normalization Forms — chờ đối chiếu, Canonical equivalence and NFC normalization — chưa đối chiếu UAX 15. Chưa đối chiếu với văn bản nguồn (chưa mở).
  6. IEEE 1800-2023 SystemVerilog LRM, String len/getc byte operations and source text handling — chưa đối chiếu LRM. Chưa đối chiếu với văn bản nguồn (chưa mở).