Skip to content

实现 校验码计算 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-inputel-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 效果截图

校验码计算 BCC/LRC/CRC 效果截图

工具访问地址:https://www.i91tools.com/tools/checksum