Investigating error detection using CRC
This example examines how Cyclic Redundancy Check (CRC) detects errors when transmitting packets over a noisy binary channel. The Engee model transmits random packets, adds CRC verification bits to them, distorts them in a binary symmetric channel, checks the syndrome on reception, and counts correct, detected, and undetected erroneous packets. The model is assembled from Engee library blocks, including the General CRC Generator and General CRC Syndrome Detector. The script manages the model programmatically and compares the simulation results with an accurate theory calculated from the weight spectrum of the code. Experiments can also be set up in a web application based on the Genie Framework, which runs the same model. The example is useful for engineers and students studying noise-tolerant coding and statistical modeling of communication channels.
In any communication channel, some of the bits are distorted by interference. In order for the receiver to know about the error, the transmitter adds redundant verification bits to the data. CRC is one of the most common methods of such verification: it is used in Ethernet, USB, industrial network protocols, in file archives and in data storage.
CRC only detects errors, but does not correct them. The receiver discards the error packet or requests it again. Therefore, the main indicator of CRC quality is the probability that an error will go unnoticed.
In this example, we:
- Let's briefly analyze the CRC theory and deduce the exact probabilities of three packet reception outcomes.
- Let's study the model and test it on experiments with a pre-known answer.
- Let's compare the simulation results with the theory in a wide range of error probability.
- Let's launch a web application in which experiments can be performed without programming.
CRC Theory
The bits of the packet are considered as coefficients of a polynomial over the field GF(2), where addition is the exclusive OR. Information Bits form a polynomial degrees less . The generating polynomial degrees set by a list of degrees: [5, 3, 0] means .
The CRC encoder assigns the remainder of the division to the data:
Full package length , at the initial settings , , . Any code word divided into without a trace.
The receiver calculates the syndrome, which is the remainder of dividing the received word by . If the channel has added an error vector , the syndrome is equal to . The error is not detected only if it is itself a non-zero codeword.
A binary symmetric channel independently inverts each bit with probability . Each package falls into exactly one of the three classes:
where — the number of weight code words (the weight range of the code). By all the error vectors are equally likely, and the proportion of undetected errors is : from for vectors with zero syndrome, it is necessary to exclude the only case without errors. For and It's almost .
Description of the model

Model crc_error_detection.engee It is compiled only from Engee library blocks and works in a fixed step. . One step is one packet, so simulation time means the packet number, not the physical time.
| Knot | Engee Blocks | Appointment |
|---|---|---|
| Data source | Bernoulli Binary Generator Payload |
random information bits at each step |
| The coder | General CRC Generator CRC Encoder |
adds the verification bits |
| Channel | Bernoulli Binary Generator Channel Noise, Logical Operator XOR Binary Symmetric Channel |
error vector with probability of one adds up to the XOR code word |
| The detector | Data Type Conversion To Double, General CRC Syndrome Detector CRC Detector |
highlights information bits and a sign of a non-zero syndrome |
| Error handling | Logical Operator (XOR, OR, NOT, AND, NOR), Sum of Elements, Mux, Data Type Conversion | generates the event vector of the package |
| Counters | Add Packet Statistics and Unit Delay Previous Totals |
accumulating events |
| Stop | Constant, Relational Operator, Logical Operator OR, Stop Simulation | the model is stopped by the limits |
The error handling node generates a packet event vector at each step.:
where — a sign of a non-zero syndrome, — a sign that at least one information bit has changed. The vector elements mean: a packet has been transmitted, an error has been detected, an error has been missed, the packet is correct, and the number of inverted bits. Adder Packet Statistics together with the delay unit Previous Totals accumulates an event vector of five counters.
Pay attention to the "package is correct" class: it requires that the information bits have not changed and the syndrome is zero. An error in only the verification bits gives a non—zero syndrome and is counted as detected - the receiver cannot distinguish it from an error in the data.
Block parameters are variables of the workspace crc_n, crc_k, crc_p, crc_seed, crc_packet_limit and crc_error_limit. They are set by the script code before each launch. The model can be reproducibly recreated using a script. resources/julia/build_model.jl.
Block Parameters
Let's connect the model management functions from resources/julia/crc_runtime.jl and the directory of polynomials, open the model and output the main parameters of the key blocks with the command engee.get_param. All paths are built from the script folder via @__DIR__.
using JSON3, Printf, Plots
gr()
resources_dir = joinpath(@__DIR__, "resources")
include(joinpath(resources_dir, "julia", "crc_runtime.jl"))
include(joinpath(resources_dir, "julia", "polynomials.jl"))
crc_open_model()
show_params = Dict(
"Payload" => ["SamplesPerFrame", "ProbabilityOfZero", "InitialSeed", "SampleTime"],
"CRC Encoder" => ["GeneratorPolynomial", "InitialStates", "DirectMethod", "ReflectInputBytes", "FinalXOR"],
"Channel Noise" => ["SamplesPerFrame", "ProbabilityOfZero", "InitialSeed", "SampleTime"],
"Binary Symmetric Channel" => ["Operator"],
"CRC Detector" => ["GeneratorPolynomial", "InitialStates", "DirectMethod"],
"Data Changed" => ["Operator"], "Any Bit Changed" => ["Operator", "Inputs"],
"Undetected" => ["Operator"], "Correct" => ["Operator"],
"Previous Totals" => ["InitialCondition", "SampleTime"],
"Stop Limits" => ["Value"], "Limit Reached" => ["Operator"])
for b in ["Payload", "CRC Encoder", "Channel Noise", "Binary Symmetric Channel", "CRC Detector", "Data Changed",
"Any Bit Changed", "Undetected", "Correct", "Previous Totals", "Stop Limits", "Limit Reached"]
prm = engee.get_param("crc_error_detection/" * b)
println(rpad(b, 26), prm["BlockPath"])
println(" "^26, join(["$k = $(prm[k])" for k in show_params[b]], "; "))
end
The parameters are set by workspace variables. The step of the sources is equal to and by one bit, therefore, per model step, each source outputs a frame from or bit. The CRC parameters correspond to the classical scheme: zero initial state, without direct method, bit reflection and final XOR. The stop threshold for the second, fourth, and fifth counters is in other words, they do not affect the stop.
The Handbook of Polynomials
File polynomials.jl It contains a table of generating polynomials of cyclic Bose—Chowdhury—Hockingham codes (BCH). For each row, a list of degrees in the model format and the supported feature are calculated: whether the CRC Engee blocks support the polynomial. Let's print the table.
println(rpad("n", 5), rpad("k", 5), rpad("t", 5), rpad("Engee blocks", 13), "polynomial (degrees)")
for x in crc_presets
println(rpad(x.n, 5), rpad(x.k, 5), rpad(x.t, 5), rpad(x.supported ? "yes" : "no", 13), x.poly)
end
@printf("Поддерживается %d из %d полиномов\n", count(x -> x.supported, crc_presets), length(crc_presets))
Column — the number of errors that the corresponding BCH code would correct. The model only detects errors, so provided as a reference. All length codes are available , and and five length codes . Six length codes with polynomials of degree from before CRC library blocks are not supported: their degree is greater .
Checking the model
File crc_runtime.jl It contains six functions:
crc_validatechecks the parameters of the experience: block length, probability, list of degrees of the polynomial, limits and seed;crc_poly_literalwrites the vector of degrees of a polynomial as a literal for CRC blocks;crc_open_modelloads the model if it is not open yet.;crc_signalfinds the recorded block signal in the simulation results;crc_run_jsonaccepts parameters as a JSON string, writes them to workspace variables and CRC block parameters, and runs the model with the commandengee.runand returns the total counters as JSON;crc_reference_encode— independent CRC encoder: dividing polynomials "into columns" to check the General CRC Generator block.
The JSON format was chosen because the web application uses the same function. Let's launch a control experiment: , , a polynomial [5, 3, 0], packages.
crc_example_config = Dict("n" => 19, "p" => 0.1, "poly" => [5, 3, 0],
"blocksToStop" => 2000, "errorsToStop" => 2000, "seed" => 12345)
crc_example_result = JSON3.read(crc_run_json(JSON3.write(crc_example_config)))
@printf("Всего %d: верно %d, обнаружено %d, не обнаружено %d; битовых ошибок %d; %s; %.1f с\n",
crc_example_result.packets, crc_example_result.correct, crc_example_result.detected,
crc_example_result.undetected, crc_example_result.bit_errors, crc_example_result.stop_reason,
crc_example_result.seconds)
@assert crc_example_result.correct + crc_example_result.detected + crc_example_result.undetected == crc_example_result.packets
Behind packages at correct accepted packages, erroneous packages were detected and skipped. This is the number of packets, not the bits: the channel has been inverted. bit from That is , under expected conditions . The percentage of correct packages coincides with the theoretical . The sum of the three classes is equal to the number of transmitted packets. This check is performed by @assert.
Now let's perform a set of control experiments. File verify_model.jl runs the model in six experiments with a pre-known answer and verifies that invalid parameters are rejected. Among the checks is a comparison of the output of the General CRC Generator block with an independent encoder crc_reference_encode on real packages of the model and checking that any single error gives a non-zero syndrome. The result is written in resources/data/verification.json.
verification = JSON3.read(include(joinpath(resources_dir, "julia", "verify_model.jl")))
println("All checks have been completed: ", verification.passed)
for t in verification.tests
if haskey(t, :result)
r = t.result
@printf(" %-48s n=%-3d p=%-4g пакетов %-5d верно %-5d обн. %-5d не обн. %d\n",
t.case, r.n, r.p, r.packets, r.correct, r.detected, r.undetected)
else
@printf(" %-48s кадров %d, одиночных ошибок %d\n", t.case, t.frames, t.single_bit_cases)
end
end
All checks have been completed:
- everything is quiet The packages were accepted correctly;
- for the code inversion of all the bits turns a code word into another code word: a word of seven units is divided into . CRC does not see such an error, and the Stop Simulation block stops the model exactly after undetected errors;
- for the length the same inversion is always found;
- the same seed reproduces all counters;
- the output of the General CRC Generator block coincided with the independent encoder on all packages, that's all single errors give a non-zero syndrome. This means that the library unit calculates the CRC exactly according to the formula from the theoretical part.;
- the polynomial of the largest supported degree from the table, it works without errors, and the polynomial of degree rejected before the model is launched.
Comparison with theory
Calculate the exact weight range of the code with a polynomial . To do this, we will encode everything with a reference encoder. Let's find the information words and calculate the weights of the code words. Function theory Based on the spectrum, it calculates the probabilities of three classes of packages.
poly = [5, 3, 0]; n_c, k_c = 19, 14
A = zeros(Int, n_c + 1) # A[w+1] is the number of codewords of weight w
for m in 0:2^k_c-1
payload = [(m >> (k_c - i)) & 1 for i in 1:k_c]
A[count(==(1), crc_reference_encode(payload, poly))+1] += 1
end
println("Weight spectrum A_w (w: number of words): ", join(["$(w):$(A[w+1])" for w in 0:n_c if A[w+1] > 0], " "))
@printf("Минимальный ненулевой вес d_min = %d\n", findfirst(>(0), A[2:end]))
theory(p) = begin
pc = (1 - p)^n_c
pu = sum(A[w+1] * p^w * (1 - p)^(n_c - w) for w in 1:n_c)
(correct = pc, undetected = pu, detected = 1 - pc - pu)
end
Minimum weight of a non-zero codeword Therefore, CRC is guaranteed to detect any single and double errors. There are many words of odd weight: a polynomial does not contain a multiplier therefore, unlike many standard CRCs, such as CRC-16, it does not detect all errors of odd multiplicity. Any error packet up to in length the bit is detected: such an error vector is not divisible by .
Let's run a model for the probabilities of bit inversion from before , by packages at each point, and compare the shares of the three classes of packages with the theory.
ps = [0.001, 0.003, 0.01, 0.03, 0.1, 0.2, 0.3, 0.5]
sim = Dict{Float64, Any}()
for p in ps
cfg = Dict("n" => 19, "p" => p, "poly" => poly, "blocksToStop" => 20000, "errorsToStop" => 1_000_000, "seed" => 777)
sim[p] = JSON3.read(crc_run_json(JSON3.write(cfg)))
th = theory(p); r = sim[p]
@printf("p = %-5g верно %.4f (теор. %.4f) обн. %.4f (%.4f) не обн. %.2e (%.2e)\n", p,
r.correct / r.packets, th.correct, r.detected / r.packets, th.detected,
r.undetected / r.packets, th.undetected)
end
The proportions of correct and detected packets match the theory up to the second or third digit in the entire range. Undetected errors are a rare event, and with small the model has not registered any. To understand whether this is consistent with theory, we need to estimate the statistical error.
Let's build two panels: the proportion of correct and detected packages on a regular scale, then the proportion of undetected errors on a logarithmic scale. For each non-zero estimate, we will show a 95% Wilson confidence interval. For a share its boundaries
If the model has not registered the event, instead of the zero invisible on the logarithmic scale, we will show the upper 95% probability limit with a triangle. Such a triangle is not a measured fraction. A horizontal line Don't draw: value applies only to , rather than the entire range .
# The theoretical probabilities are calculated from the exact weight spectrum A_w.
pgrid = 10 .^ range(-3, log10(0.5); length = 200)
function wilson95(k, n)
z = 1.959963984540054
q = k / n
den = 1 + z^2 / n
mid = (q + z^2 / (2n)) / den
half = z * sqrt(q * (1 - q) / n + z^2 / (4n^2)) / den
(max(0.0, mid - half), min(1.0, mid + half))
end
main_plot = plot(pgrid, [theory(p).correct for p in pgrid];
color = :seagreen, lw = 2, label = "That's right, theory", xscale = :log10,
ylims = (-0.03, 1.05), ylabel = "Share of packages",
title = "CRC x^5+x^3+1, n =19: model and exact theory",
legend = :outertopright, left_margin = 5Plots.mm)
plot!(main_plot, pgrid, [theory(p).detected for p in pgrid];
color = :royalblue, lw = 2, label = "discovered, theory")
scatter!(main_plot, ps, [sim[p].correct / sim[p].packets for p in ps];
color = :seagreen, marker = :circle, ms = 6, label = "That's right, the model")
scatter!(main_plot, ps, [sim[p].detected / sim[p].packets for p in ps];
color = :royalblue, marker = :square, ms = 6, label = "detected, the model")
rare_plot = plot(pgrid, [theory(p).undetected for p in pgrid];
color = :orangered, lw = 2, label = "not detected, theory",
xscale = :log10, yscale = :log10, ylims = (1e-8, 0.1),
xlabel = "Probability of inversion of the p bit", ylabel = "Share of packages",
legend = :outertopright, left_margin = 5Plots.mm)
nonzero_ps = [p for p in ps if sim[p].undetected > 0]
zero_ps = [p for p in ps if sim[p].undetected == 0]
for (j, p) in enumerate(nonzero_ps)
r = sim[p]
low, high = wilson95(r.undetected, r.packets)
plot!(rare_plot, [p, p], [low, high]; color = :orangered, lw = 2,
label = j == 1 ? "95% интервал Уилсона" : "")
end
scatter!(rare_plot, nonzero_ps,
[sim[p].undetected / sim[p].packets for p in nonzero_ps];
color = :orangered, marker = :diamond, ms = 6, label = "not detected, the model")
scatter!(rare_plot, zero_ps,
[wilson95(0, sim[p].packets)[2] for p in zero_ps];
color = :orangered, marker = :dtriangle, ms = 7,
label = "0 events: upper 95% limit")
println("A 95% interval is expected for the share of events within")
for p in ps
r = sim[p]
low, high = wilson95(r.undetected, r.packets)
expected = r.packets * theory(p).undetected
@printf("%-7g %6d %7.3f [%.3g, %.3g] %s\n",
p, r.undetected, expected, low, high,
low <= theory(p).undetected <= high ? "да" : "нет")
end
@assert all(p -> begin
r = sim[p]
low, high = wilson95(r.undetected, r.packets)
low <= theory(p).undetected <= high
end, ps)
plot(main_plot, rare_plot; layout = (2, 1), size = (1050, 760))
The model is consistent with the exact theory within the statistical margin of error. The upper panel shows the matching of the correct and detected packets. In the lower panel, the intervals around the non-zero estimates contain a theoretical curve, and the table confirms this for all eight points.
By , and the exact theory expects accordingly about , and an undetected packet from . The probability of getting zero events at compose . Therefore, zero counters do not contradict the theory: for such a lot more packages are needed.
By the model gave against theoretical ones (about expected events). The uncertainty interval includes the theoretical value. By the exact value , the model gave about .
Web application based on Genie Framework
Genie Framework — a set of packages for creating web applications:
Genie.jl— a web server with query routing, HTML and JSON output;Stipple.jl— a reactive interface that binds script variables to page elements;GenieFramework.jl— a common package that connects everything at once and gives a macro@genietools.
Engee launches Genie applications in the cloud: team engee.genie.start(папка) lifts the server from the file app.jl in the specified folder and returns the page address. The application code is executed in a separate process. To execute the command in the main Engee session where the model is loaded, use the function engee.genie.eval(строка_кода).
The application of this example is in the folder resources/CRCApp:
index.html— a page with experience parameter fields: block size, error probability, polynomial, stop limits, and seed. The buttons launch the experience, clear the history, and upload the CSV. The history is stored insessionStoragebrowser tabs.- The route
/inapp.jlgives this page as it is. The interface is written in pure JavaScript, so the Stipple reactive model is not needed here. - The route
POST /api/runaccepts parameters in JSON and checks each field with a functionvalidate_request. Then he callsengee.genie.eval("crc_run_json(...)"): the model is launched in the main session, and the counters are returned to the page. - Blocking
ReentrantLockit does not allow you to start the second experience while the first one is running. - User input is never executed as code: only the verified JSON string is passed to the main session as a string literal.
Each row of the table in the application is the result of a real model launch. crc_error_detection.engee, not an analytical calculation. Let's launch the app. File start.jl connects control functions, opens the model, and calls engee.genie.start.
include(joinpath(resources_dir, "julia", "start.jl"))
app_url = match(r"https://[^\s']+?/genie/CRCApp/", repr(crc_app_status))
println("The application is running: ", isnothing(app_url) ? crc_app_status : app_url.match)
The cell displays the address of the application in the form https://engee.com/prod/user/<имя_пользователя>/genie/CRCApp/. Open it in a browser. This is what the shape of the experience parameters looks like.:

After starting the model, the result appears in the table. The picture below shows the experience with the initial parameters.: packages at . The percentage of correct packages coincides with the theoretical . After launch, the model remains open for further experiments in the script and in the application.

The last cell returns a summary for the point .
th = theory(0.1); r = sim[0.1]
string("CRC ok; p=0.1: correct=", round(r.correct / r.packets, digits = 4), " (theory ", round(th.correct, digits = 4),
"), undetected=", round(r.undetected / r.packets, sigdigits = 3), " (theory ", round(th.undetected, sigdigits = 3), ")")
Conclusion
We investigated error detection using CRC on the Engee model from library blocks and compared the simulation results with the exact theory. The code with a polynomial and the length of the package the bit is guaranteed to detect single and double errors, since its minimum weight is , as well as error packets up to in length bit. The proportions of correct, detected, and undetected packets coincided with the theory calculated from the weight spectrum of the code, over the entire range from before . By CRC skips about packets, and in a completely noisy channel — . The probability of missing an error is determined by the degree of the polynomial and its weight spectrum, therefore, higher degree polynomials are used in reliable systems.
This approach — a model, software checks, comparison with theory, and an interactive application — is useful for laboratory work and for the engineering evaluation of error-detecting systems.
