Appearance
实现 校验码计算 BCC/LRC/CRC 工具
分类:general | 标签:校验、CRC、BCC BCC LRC CRC8/16/32 校验码计算
2.1 功能说明
BCC LRC CRC8/16/32 校验码计算
核心能力
- hex
- text
- 算法
- 计算方式
- 结果
2.1.1 使用指南
功能说明
校验码用于检测数据在传输或存储过程中是否发生错误,是串口通信、工业总线、嵌入式协议中的必备环节。本工具一次性给出同一份报文的 BCC、LRC 以及十余种标准 CRC 结果,并支持自定义 CRC 参数,所有计算在浏览器本地完成,报文不会离开你的设备。
输入格式
选择「十六进制报文」时,输入内容会按字节解析,分隔符非常宽松:空格、逗号、分号、冒号、换行、0x 前缀都可以混用,也可以完全不加分隔符连续书写(此时按每两位一字节切分,总长度必须为偶数)。选择「文本」时,输入内容会先按 UTF-8 编码成字节再计算,这一点很重要——同一段中文用 GBK 编码算出的 CRC 与 UTF-8 并不相同。
算法说明
BCC(块校验码):把所有字节逐个异或,实现最简单,能检出奇数位错误,常见于门禁、POS、串口私有协议。LRC(纵向冗余校验):先对所有字节求和取低 8 位,再取反加一(即求补码),使得数据加校验码之和的低 8 位为零,Modbus ASCII 模式采用此算法。
CRC(循环冗余校验)基于二进制多项式模二除法,检错能力远强于前两者。一个 CRC 算法由五个参数唯一确定:多项式 poly、初始值 init、输入是否按位反射 refin、输出是否按位反射 refout、以及输出异或值 xorout。参数不同结果完全不同,因此对接协议时务必确认对方使用的具体变体。本工具采用查表法实现,为每个多项式预生成 256 项查找表,逐字节推进,比逐位计算快约八倍。所有内置变体均已通过业界标准校验值验证:以字符串 123456789 为输入,CRC-32/IEEE 应得 0xCBF43926,CRC-16/MODBUS 应得 0x4B37,CRC-8 应得 0xF4。
字节序与常见坑
结果列同时给出了「字节序反转」值。Modbus RTU 协议在报文中先发 CRC 低字节再发高字节,也就是说计算得到 0x4B37 时,实际上线的字节是 37 4B,直接照抄高位在前的写法是最常见的对接失败原因。另一个常见问题是校验范围搞错:多数协议的 CRC 只覆盖地址域到数据域,不包含帧头、帧尾与 CRC 自身,请按协议文档裁剪好报文再计算。
使用场景
Modbus RTU 与 ASCII 报文调试、串口与 485 总线协议对接、单片机固件通信校验、文件完整性快速比对、以及协议逆向分析时反推校验算法。
2.2 代码实现
2.2.1 组件结构
本工具是一个基于 Vue 3 <script setup> 语法的单文件组件(SFC),统一包裹在 ToolPageShell 组件内,由它提供页面标题、工具 ID、分类与「使用指南」插槽等通用外壳;核心业务逻辑(响应式状态、计算属性、事件处理函数)全部写在 <script setup> 中,输入/输出通过 el-input、el-button 等 Element Plus 组件与用户交互,所有数据均在浏览器本地处理,不会上传服务器。
2.2.2 核心逻辑概览
<script setup> 中定义的主要函数/方法:
buildTable()reflectBits()crcCompute()toHex()swapBytes()buildReport()showHint()copyText()copyReport()downloadReport()loadSample()clearAll()
2.2.3 关键实现代码
以下是该工具的完整 <script setup> 实现(核心代码)。模板(<template>)与样式(<style>)已省略。
vue
import { computed, reactive, ref } from 'vue'
import ToolPageShell from '@/components/ToolPageShell.vue'
/* ------------------------- CRC 查表法核心实现 ------------------------- */
const tableCache = new Map()
function buildTable(width, poly) {
const key = width + ':' + poly
const cached = tableCache.get(key)
if (cached) return cached
const shift = width - 8
const topBit = width === 32 ? 0x80000000 : (1 << (width - 1))
const mask = width === 32 ? 0xffffffff : ((1 << width) - 1)
const table = new Uint32Array(256)
for (let i = 0; i < 256; i++) {
let crc = ((i << shift) & mask) >>> 0
for (let bit = 0; bit < 8; bit++) {
if ((crc & topBit) !== 0) crc = ((((crc << 1) >>> 0) ^ poly) & mask) >>> 0
else crc = (((crc << 1) >>> 0) & mask) >>> 0
}
table[i] = crc >>> 0
}
tableCache.set(key, table)
return table
}
function reflectBits(value, width) {
let result = 0
for (let i = 0; i < width; i++) {
result = (((result << 1) >>> 0) | ((value >>> i) & 1)) >>> 0
}
return result >>> 0
}
const REFLECT8 = new Uint8Array(256)
for (let i = 0; i < 256; i++) REFLECT8[i] = reflectBits(i, 8)
function crcCompute(data, spec) {
const { width, poly, init, refin, refout, xorout } = spec
const table = buildTable(width, poly)
const shift = width - 8
const mask = width === 32 ? 0xffffffff : ((1 << width) - 1)
let crc = (init & mask) >>> 0
for (let i = 0; i < data.length; i++) {
const byte = refin ? REFLECT8[data[i]] : data[i]
const index = ((crc >>> shift) ^ byte) & 0xff
crc = ((((crc << 8) >>> 0) ^ table[index]) & mask) >>> 0
}
if (refout) crc = reflectBits(crc, width)
return ((crc ^ xorout) & mask) >>> 0
}
const CRC_PRESETS = [
{ name: 'CRC-8', width: 8, poly: 0x07, init: 0x00, refin: false, refout: false, xorout: 0x00 },
{ name: 'CRC-8/MAXIM (Dallas 1-Wire)', width: 8, poly: 0x31, init: 0x00, refin: true, refout: true, xorout: 0x00 },
{ name: 'CRC-8/ROHC', width: 8, poly: 0x07, init: 0xff, refin: true, refout: true, xorout: 0x00 },
{ name: 'CRC-8/ITU', width: 8, poly: 0x07, init: 0x00, refin: false, refout: false, xorout: 0x55 },
{ name: 'CRC-16/MODBUS', width: 16, poly: 0x8005, init: 0xffff, refin: true, refout: true, xorout: 0x0000 },
{ name: 'CRC-16/XMODEM', width: 16, poly: 0x1021, init: 0x0000, refin: false, refout: false, xorout: 0x0000 },
{ name: 'CRC-16/CCITT-FALSE', width: 16, poly: 0x1021, init: 0xffff, refin: false, refout: false, xorout: 0x0000 },
{ name: 'CRC-16/KERMIT (CCITT)', width: 16, poly: 0x1021, init: 0x0000, refin: true, refout: true, xorout: 0x0000 },
{ name: 'CRC-16/ARC (IBM)', width: 16, poly: 0x8005, init: 0x0000, refin: true, refout: true, xorout: 0x0000 },
{ name: 'CRC-16/USB', width: 16, poly: 0x8005, init: 0xffff, refin: true, refout: true, xorout: 0xffff },
{ name: 'CRC-32/IEEE 802.3', width: 32, poly: 0x04c11db7, init: 0xffffffff, refin: true, refout: true, xorout: 0xffffffff },
{ name: 'CRC-32/BZIP2(不反射)', width: 32, poly: 0x04c11db7, init: 0xffffffff, refin: false, refout: false, xorout: 0xffffffff },
{ name: 'CRC-32/MPEG-2', width: 32, poly: 0x04c11db7, init: 0xffffffff, refin: false, refout: false, xorout: 0x00000000 },
{ name: 'CRC-32C (Castagnoli)', width: 32, poly: 0x1edc6f41, init: 0xffffffff, refin: true, refout: true, xorout: 0xffffffff }
]
/* ------------------------------ 组件状态 ------------------------------ */
const inputType = ref('hex')
const input = ref('')
const hint = ref('')
const custom = reactive({
width: 16,
poly: '1021',
init: 'FFFF',
xorout: '0000',
refin: false,
refout: false
})
function toHex(value, width) {
return '0x' + (value >>> 0).toString(16).toUpperCase().padStart(width / 4, '0')
}
// 按位宽做字节序反转,便于对照 Modbus 等低字节在前的协议
function swapBytes(value, width) {
if (width === 8) return value & 0xff
if (width === 16) return (((value & 0xff) << 8) | ((value >>> 8) & 0xff)) >>> 0
return ((((value & 0xff) << 24) >>> 0) |
(((value >>> 8) & 0xff) << 16) |
(((value >>> 16) & 0xff) << 8) |
((value >>> 24) & 0xff)) >>> 0
}
// 解析结果与错误一起返回,避免在计算属性中产生副作用
const parsed = computed(() => {
const raw = input.value
if (!raw.trim()) return { data: new Uint8Array(0), error: '' }
if (inputType.value === 'text') {
return { data: new TextEncoder().encode(raw), error: '' }
}
const cleaned = raw.replace(/0[xX]/g, ' ').replace(/[\s,;:_\-|]+/g, ' ').trim()
if (!cleaned) return { data: new Uint8Array(0), error: '' }
const invalid = cleaned.replace(/[0-9a-fA-F ]/g, '')
if (invalid) {
const chars = Array.from(new Set(Array.from(invalid))).join(' ')
return { data: new Uint8Array(0), error: '存在非十六进制字符:' + chars }
}
const tokens = cleaned.split(' ').filter(Boolean)
const list = []
for (const token of tokens) {
if (token.length <= 2) {
list.push(parseInt(token, 16))
continue
}
if (token.length % 2 !== 0) {
return { data: new Uint8Array(0), error: '片段 ' + token + ' 长度为奇数,无法按字节切分' }
}
for (let i = 0; i < token.length; i += 2) {
list.push(parseInt(token.slice(i, i + 2), 16))
}
}
return { data: new Uint8Array(list), error: '' }
})
const bytes = computed(() => parsed.value.data)
const parseError = computed(() => parsed.value.error)
const bytesPreview = computed(() => {
const list = Array.from(bytes.value.slice(0, 24))
.map(b => b.toString(16).toUpperCase().padStart(2, '0'))
return list.join(' ') + (bytes.value.length > 24 ? ' ...' : '')
})
const simpleResults = computed(() => {
const data = bytes.value
if (!data.length) return []
let bcc = 0
let sum = 0
for (let i = 0; i < data.length; i++) {
bcc ^= data[i]
sum = (sum + data[i]) & 0xff
}
const lrc = (((sum ^ 0xff) + 1) & 0xff)
return [
{ name: 'BCC 异或校验', desc: '所有字节逐个按位异或', value: toHex(bcc, 8) },
{ name: 'LRC 纵向冗余', desc: '求和取低八位后取反加一(补码)', value: toHex(lrc, 8) },
{ name: '累加和 SUM', desc: '所有字节求和后取低八位', value: toHex(sum, 8) },
{ name: '字节总数', desc: '参与计算的数据长度', value: String(data.length) }
]
})
const crcResults = computed(() => {
const data = bytes.value
if (!data.length) return []
return CRC_PRESETS.map(preset => {
const value = crcCompute(data, preset)
return {
name: preset.name,
params: [
toHex(preset.poly, preset.width),
toHex(preset.init, preset.width),
preset.refin ? '是' : '否',
preset.refout ? '是' : '否',
toHex(preset.xorout, preset.width)
].join(' / '),
value: toHex(value, preset.width),
swapped: toHex(swapBytes(value, preset.width), preset.width)
}
})
})
const customResult = computed(() => {
const data = bytes.value
if (!data.length) return { value: '', decimal: '', error: '' }
const width = custom.width
const maxHexLength = width / 4
const fields = [
['多项式', custom.poly],
['初始值', custom.init],
['输出异或', custom.xorout]
]
const parsed = []
for (const [label, text] of fields) {
const cleaned = String(text).trim().replace(/^0[xX]/, '')
if (!cleaned) { parsed.push(0); continue }
if (!/^[0-9a-fA-F]+$/.test(cleaned)) {
return { value: '', decimal: '', error: label + ' 不是合法的十六进制数' }
}
if (cleaned.length > maxHexLength) {
return { value: '', decimal: '', error: label + ' 超出 CRC-' + width + ' 的取值范围' }
}
parsed.push(parseInt(cleaned, 16) >>> 0)
}
const value = crcCompute(data, {
width,
poly: parsed[0],
init: parsed[1],
refin: custom.refin,
refout: custom.refout,
xorout: parsed[2]
})
return { value: toHex(value, width), decimal: String(value >>> 0), error: '' }
})
function buildReport() {
const lines = []
lines.push('校验码计算结果')
lines.push('输入格式:' + (inputType.value === 'hex' ? '十六进制报文' : '文本 UTF-8'))
lines.push('字节长度:' + bytes.value.length)
lines.push('报文预览:' + bytesPreview.value)
lines.push('')
lines.push('[简单校验]')
for (const row of simpleResults.value) lines.push(row.name + ':' + row.value)
lines.push('')
lines.push('[CRC 标准算法]')
for (const row of crcResults.value) {
lines.push(row.name + ':' + row.value + '(字节序反转 ' + row.swapped + ')')
lines.push(' 参数 ' + row.params)
}
if (!customResult.value.error && customResult.value.value) {
lines.push('')
lines.push('[自定义 CRC-' + custom.width + ']')
lines.push('参数 poly=' + custom.poly + ' init=' + custom.init + ' xorout=' + custom.xorout +
' refin=' + (custom.refin ? '是' : '否') + ' refout=' + (custom.refout ? '是' : '否'))
lines.push('结果 ' + customResult.value.value)
}
return lines.join('\n')
}
function showHint(text) {
hint.value = text
window.setTimeout(() => { hint.value = '' }, 2000)
}
async function copyText(text) {
if (!text) return
try {
await navigator.clipboard.writeText(text)
showHint('已复制 ' + text)
} catch {
showHint('复制失败,请手动选择文本复制')
}
}
function copyReport() {
copyText(buildReport())
}
function downloadReport() {
const blob = new Blob([buildReport()], { type: 'text/plain;charset=utf-8' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = 'checksum-report.txt'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
showHint('已开始下载')
}
function loadSample() {
inputType.value = 'hex'
input.value = '01 03 00 00 00 0A'
}
function clearAll() {
input.value = ''
}2.3 效果截图
