mirror of
https://github.com/len0rd/personal-website.git
synced 2025-02-28 19:42:15 -05:00
add post about working with canbus dbc files
This commit is contained in:
parent
5d805751a9
commit
70305030d6
75
conf.py
75
conf.py
|
@ -1,5 +1,8 @@
|
|||
# Sphinx docs configuration for building project documentation
|
||||
from datetime import datetime
|
||||
from pygments.lexer import RegexLexer, bygroups
|
||||
from pygments import token
|
||||
from sphinx.highlighting import lexers
|
||||
|
||||
project = "lenordsNet"
|
||||
author = "lenord"
|
||||
|
@ -10,6 +13,7 @@ extensions = [
|
|||
"ablog",
|
||||
"sphinx.ext.intersphinx",
|
||||
"sphinx_design",
|
||||
"sphinxcontrib.bitfield",
|
||||
]
|
||||
|
||||
templates_path = ["_templates"]
|
||||
|
@ -51,3 +55,74 @@ pygments_style = "sas"
|
|||
|
||||
drawio_headless = True
|
||||
drawio_no_sandbox = True
|
||||
|
||||
|
||||
class CanbusDbcLexer(RegexLexer):
|
||||
name = "DBC"
|
||||
|
||||
tokens = {
|
||||
'root' : [
|
||||
(r'[:\|\[\],\(\)]', token.Punctuation),
|
||||
(r'[\@\-\+]', token.Operator),
|
||||
("CM_", token.Keyword, 'comment'),
|
||||
("SG_", token.Keyword, "signal"),
|
||||
("BO_", token.Keyword, 'msg'),
|
||||
("BA_DEF_DEF_", token.Keyword, 'attrdefault'),
|
||||
("BA_DEF_", token.Keyword, 'attrdef'),
|
||||
('BA_', token.Keyword, 'sigdefault')
|
||||
],
|
||||
'msg': [
|
||||
(r'(\s+)(\d+)(\s+)([\w_]+)(\s*?)(:)(\s+)(\d+)(\s+)(\w+)',
|
||||
bygroups(token.Whitespace, token.Number, token.Whitespace, token.Name, token.Whitespace, token.Punctuation, token.Whitespace, token.Number, token.Whitespace, token.Name)),
|
||||
],
|
||||
'signal': [
|
||||
(r'(\s+)([\w_]+)(\s+)(:)(\s+)(\d+)(\|)(\d+)(@)(\d+)([+-])(\s+)(\()([-\d\.]+)(,)([-\d\.]+)(\))(\s+)(\[)([\d-]+)(\|)([\d-]+)(\])(\s+)(".*?")(.*?\n)',
|
||||
bygroups(token.Whitespace,
|
||||
token.Name,
|
||||
token.Whitespace,
|
||||
token.Punctuation,
|
||||
token.Whitespace,
|
||||
token.Number,
|
||||
token.Punctuation,
|
||||
token.Number,
|
||||
token.Punctuation,
|
||||
token.Number,
|
||||
token.Punctuation,
|
||||
token.Whitespace,
|
||||
token.Punctuation,
|
||||
token.Number,
|
||||
token.Punctuation,
|
||||
token.Number,
|
||||
token.Punctuation,
|
||||
token.Whitespace,
|
||||
token.Punctuation,
|
||||
token.Number,
|
||||
token.Punctuation,
|
||||
token.Number,
|
||||
token.Punctuation,
|
||||
token.Whitespace,
|
||||
token.String,
|
||||
token.Name,
|
||||
)),
|
||||
],
|
||||
'comment': [
|
||||
(r'(\s+)(BO_)(\s+)(\d+)(\s+)(".*?")(;)',
|
||||
bygroups(token.Whitespace, token.Keyword, token.Whitespace, token.Number, token.Whitespace, token.String, token.Punctuation)),
|
||||
(r'(\s+)(SG_)(\s+)(\d+)(\s+)(\w+)(\s+)(".*?")(;)',
|
||||
bygroups(token.Whitespace, token.Keyword, token.Whitespace, token.Number, token.Whitespace, token.Name, token.Whitespace, token.String, token.Punctuation))
|
||||
],
|
||||
'attrdef': [
|
||||
(r'(\s+)([\w_]+)(\s+)(".*?")(\s+)(\w+)(\s+)(.*?)(;)',
|
||||
bygroups(token.Whitespace, token.Keyword, token.Whitespace, token.String, token.Whitespace, token.Keyword, token.Whitespace, token.Name, token.Punctuation))
|
||||
],
|
||||
'attrdefault': [
|
||||
(r'(\s+)(".*?")(\s+)(.*?)(;)',
|
||||
bygroups(token.Whitespace, token.String, token.Whitespace, token.Name, token.Punctuation))
|
||||
],
|
||||
'sigdefault': [
|
||||
(r'(\s+)(".*?")(\s+)(SG_)(\s+)(\d+)(\s+)(\S+)(\s+)([0-9\-\.]+)(;)',
|
||||
bygroups(token.Whitespace, token.String, token.Whitespace, token.Keyword, token.Whitespace, token.Number, token.Whitespace, token.Name, token.Whitespace, token.Number, token.Punctuation))
|
||||
],
|
||||
}
|
||||
|
||||
lexers['dbc'] = CanbusDbcLexer(startinline=True)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/bash
|
||||
|
||||
ablog build
|
||||
python -m http.server -d /website/_website/ 8090
|
||||
python -m http.server -d _website/ 8090
|
||||
|
|
|
@ -3,3 +3,6 @@ ablog
|
|||
sphinx-design
|
||||
sphinxcontrib-youtube
|
||||
pydata-sphinx-theme
|
||||
# for bitfield diagram generation (with latex support)
|
||||
# installing from a commit on master since recently merged fixes have not been made into a versioned release yet
|
||||
https://github.com/Arth-ur/sphinxcontrib-bitfield/archive/e1bf4809d024597919cd99820d48e626bc1fce3c.zip
|
||||
|
|
219
posts/canbus_dbc_format.rst
Normal file
219
posts/canbus_dbc_format.rst
Normal file
|
@ -0,0 +1,219 @@
|
|||
|
||||
|
||||
Defining CANBus messages with a DBC file
|
||||
========================================
|
||||
|
||||
.. post:: 03, December 2024
|
||||
:tags: embedded, development, toolchain,
|
||||
:category: Projects
|
||||
:author: len0rd
|
||||
|
||||
|
||||
DBC files are a standard way to define the messages that will be transmitted over a raw CANBus.
|
||||
While working with this format I found it easy to find tools that could interpret, use, and generate DBC files.
|
||||
However I had a hard time finding resources to describe the format and schema of these files. This post describes
|
||||
some of the schema details I found the most helpful, many of which I had to learn by digging through the source
|
||||
code of tools that work with DBC files. Personally, I find examples the easiest way to learn/understand
|
||||
schema basics, so I'll use that here while also describing the schema itself.
|
||||
|
||||
Here are a few useful resources I found while trying to work with DBC files:
|
||||
|
||||
- This repository provides a useful overview of the DBC spec along with a basic example DBC file: https://github.com/stefanhoelzl/CANpy
|
||||
- This repository can serialize/deserialize DBC files into python objects, generate code from them, etc: https://github.com/cantools/cantools
|
||||
|
||||
|
||||
Defining a message
|
||||
------------------
|
||||
|
||||
Defining a message is a well-documented, core function of DBC files. Here's an example:
|
||||
|
||||
.. code-block:: dbc
|
||||
:linenos:
|
||||
|
||||
BO_ 608 TEMPERATURE_RH: 8 UNITB
|
||||
SG_ TMPD : 0|1@1+ (1,0) [0|1] "" UNITA
|
||||
SG_ RHD : 1|1@1+ (1,0) [0|1] "" UNITA
|
||||
SG_ FLT1 : 2|1@1+ (1,0) [0|1] "" UNITA
|
||||
SG_ FLT2 : 3|1@1+ (1,0) [0|1] "" UNITA
|
||||
SG_ TEMP1 : 16|16@1- (0.1,0) [-32768|32767] "C" UNITA
|
||||
SG_ TEMP2 : 32|16@1- (0.1,0) [-32768|32767] "C" UNITA
|
||||
SG_ RH1 : 48|8@1+ (1,0) [0|255] "%" UNITA
|
||||
SG_ RH2 : 56|8@1+ (1,0) [0|255] "%" UNITA
|
||||
|
||||
|
||||
| ``BO_`` is used to define a new message.
|
||||
| Format: ``BO_ <CAN-ID> <MessageName>: <MessageLength> <SendingNode>``
|
||||
|
||||
So in the above example, this message has a CAN frame ID of ``608``, is named "TEMPERATURE_RH", is ``8`` bytes in length and is sent by a CAN network node named "UNITB".
|
||||
|
||||
| Within the "TEMERATURE_RH" message, a number of "signals" (aka message fields) are defined, using ``SG_``.
|
||||
| Format: ``SG_ <SignalName> : <StartBit>|<Length>@<Endianness><Signed> (<Factor>,<Offset>) [<Min>|<Max>] "[Unit]" [ReceivingNodes]``
|
||||
|
||||
Given the above message definition, the CAN payload will look like this:
|
||||
|
||||
.. bitfield::
|
||||
:bits: 64
|
||||
:lanes: 2
|
||||
:vflip:
|
||||
:vspace: 192
|
||||
:fontfamily: monospace
|
||||
:caption: TEMPERATURE_RH bitmap
|
||||
|
||||
[
|
||||
{ "name": "TMPD", "rotate": "-90", "bits": 1, "type": 1},
|
||||
{ "name": "RHD", "rotate": "-90", "bits": 1, "type": 1},
|
||||
{ "name": "FLT1", "rotate": "-90", "bits": 1, "type": 1},
|
||||
{ "name": "FLT2", "rotate": "-90", "bits": 1, "type": 1},
|
||||
{ "name": "reserved", "bits": 12, "type": 5},
|
||||
{ "name": "TEMP1", "bits": 16, "type": 1},
|
||||
{ "name": "TEMP2", "bits": 16, "type": 1},
|
||||
{ "name": "RH1", "bits": 8, "type": 1},
|
||||
{ "name": "RH2", "bits": 8, "type": 1}
|
||||
]
|
||||
|
||||
And here's one way it could look in a C++ struct representation:
|
||||
|
||||
.. code-block:: cpp
|
||||
:linenos:
|
||||
|
||||
struct TemperatureRh {
|
||||
uint8_t TMPD : 1;
|
||||
uint8_t RHD : 1;
|
||||
uint8_t FLT1 : 1;
|
||||
uint8_t FLT2 : 1;
|
||||
uint16_t _reserved : 12;
|
||||
int16_t TEMP1;
|
||||
int16_t TEMP2;
|
||||
uint8_t RH1;
|
||||
uint8_t RH2;
|
||||
|
||||
constexpr size_t WIRE_SIZE_BYTES = 8;
|
||||
} __attribute__((packed));
|
||||
|
||||
|
||||
Node names are arbitrary, but the list of possible nodes should be defined at the top of a DBC file using ``BU_``
|
||||
|
||||
.. code-block:: dbc
|
||||
|
||||
BU_: UNITB UNITA
|
||||
|
||||
|
||||
Documenting messages
|
||||
--------------------
|
||||
|
||||
Documenting messages and signals is done using the ``CM_`` keyword:
|
||||
|
||||
.. code-block:: dbc
|
||||
:linenos:
|
||||
|
||||
CM_ BO_ 608 "Current Temp / % RH.";
|
||||
|
||||
CM_ SG_ 608 TMPD "Set if Sensor 1 / Sensor 2 Temp data differs by more than 3 degC";
|
||||
CM_ SG_ 608 RHD "Set if Sensor 1 / Sensor 2 % RH data differs by more than 5%";
|
||||
CM_ SG_ 608 FLT1 "Set if Sensor 1's diagnostics indicate an error";
|
||||
CM_ SG_ 608 FLT2 "Set if Sensor 2's diagnostics indicate an error";
|
||||
CM_ SG_ 608 TEMP1 "Sensor 1's temp reading as a signed 16-bit value. **LSB =** 0.1 degC";
|
||||
CM_ SG_ 608 TEMP2 "Sensor 2's temp reading as a signed 16-bit value. **LSB =** 0.1 degC";
|
||||
CM_ SG_ 608 RH1 "Sensor 1's % RH value as an unsigned 8-bit value. **LSB =** 1%";
|
||||
CM_ SG_ 608 RH2 "Sensor 2's % RH value as an unsigned 8-bit value.
|
||||
**LSB =** 1%";
|
||||
|
||||
Format: ``CM_ [<BU_|BO_|SG_> [CAN-ID] [SignalName]] "<DescriptionText>";``
|
||||
|
||||
Since these specify the specific signal/message they apply to, you can place them anywhere in a DBC file. Note comments can span multiple lines.
|
||||
|
||||
|
||||
Specifying default values for signals
|
||||
-------------------------------------
|
||||
|
||||
Sometimes it can be helpful to define default initial values of certain signals in a message. This can be done using an "Attribute" named "GenSigStartValue".
|
||||
|
||||
In order to use attributes, you need to first define them using ``BA_DEF_``. Its format looks like this:
|
||||
|
||||
``BA_DEF_ [BU_|BO_|SG_] "<AttributeName>" <DataType> [Config];``
|
||||
|
||||
The data in ``[Config]`` is dependent on the ``<DataType>``. of the attribute being defined. "GenSigStartValue" is a ``INT`` type, so the format of ``[Config]`` will be ``<min> <max>``.
|
||||
I think in the case of "GenSigStartValue", the min and max values dont really matter (at least it doesnt when using a lenient parser like `cantools <https://github.com/cantools/cantools>`_ )
|
||||
|
||||
You can define a default value for an attribute using ``BA_DEF_DEF_``.
|
||||
|
||||
.. code-block:: dbc
|
||||
|
||||
BA_DEF_ SG_ "GenSigStartValue" INT -100000 100000;
|
||||
BA_DEF_DEF_ "GenSigStartValue" 0;
|
||||
|
||||
This definitions need only be made once per DBC file.
|
||||
|
||||
Once you've defined the attribute, you can use it to set default/initial values for signals. For instance, using our message from earlier:
|
||||
|
||||
.. code-block:: dbc
|
||||
|
||||
BA_ "GenSigStartValue" SG_ 608 TEMP1 -32768;
|
||||
BA_ "GenSigStartValue" SG_ 608 RH1 255;
|
||||
BA_ "GenSigStartValue" SG_ 608 RH2 254;
|
||||
|
||||
Using this information to generate a C++ struct representation may look something like this:
|
||||
|
||||
|
||||
.. code-block:: cpp
|
||||
:linenos:
|
||||
|
||||
struct TemperatureRh {
|
||||
uint8_t TMPD : 1;
|
||||
uint8_t RHD : 1;
|
||||
uint8_t FLT1 : 1;
|
||||
uint8_t FLT2 : 1;
|
||||
uint16_t _reserved : 12;
|
||||
int16_t TEMP1 = -32768;
|
||||
int16_t TEMP2;
|
||||
uint8_t RH1 = 255;
|
||||
uint8_t RH2 = 254;
|
||||
|
||||
constexpr size_t WIRE_SIZE_BYTES = 8;
|
||||
} __attribute__((packed));
|
||||
|
||||
|
||||
Bringing it all together
|
||||
------------------------
|
||||
|
||||
We've now defined, commented, and set some default values for a single message. If this were the only message in a DBC file, the file would look like this:
|
||||
|
||||
.. code-block:: dbc
|
||||
:linenos:
|
||||
|
||||
VERSION ""
|
||||
|
||||
BA_DEF_ SG_ "GenSigStartValue" INT -100000 100000;
|
||||
BA_DEF_DEF_ "GenSigStartValue" 0;
|
||||
|
||||
BU_: UNITA UNITB
|
||||
|
||||
BO_ 608 TEMPERATURE_RH: 8 UNITB
|
||||
SG_ TMPD : 0|1@1+ (1,0) [0|1] "" UNITA
|
||||
SG_ RHD : 1|1@1+ (1,0) [0|1] "" UNITA
|
||||
SG_ FLT1 : 2|1@1+ (1,0) [0|1] "" UNITA
|
||||
SG_ FLT2 : 3|1@1+ (1,0) [0|1] "" UNITA
|
||||
SG_ TEMP1 : 16|16@1- (0.1,0) [-32768|32767] "C" UNITA
|
||||
SG_ TEMP2 : 32|16@1- (0.1,0) [-32768|32767] "C" UNITA
|
||||
SG_ RH1 : 48|8@1+ (1,0) [0|255] "%" UNITA
|
||||
SG_ RH2 : 56|8@1+ (1,0) [0|255] "%" UNITA
|
||||
|
||||
CM_ BO_ 608 "Current Temp / % RH.";
|
||||
|
||||
CM_ SG_ 608 TMPD "Set if Sensor 1 / Sensor 2 Temp data differs by more than 3 degC";
|
||||
CM_ SG_ 608 RHD "Set if Sensor 1 / Sensor 2 % RH data differs by more than 5%";
|
||||
CM_ SG_ 608 FLT1 "Set if Sensor 1's diagnostics indicate an error";
|
||||
CM_ SG_ 608 FLT2 "Set if Sensor 2's diagnostics indicate an error";
|
||||
CM_ SG_ 608 TEMP1 "Sensor 1's temp reading as a signed 16-bit value. **LSB =** 0.1 degC";
|
||||
CM_ SG_ 608 TEMP2 "Sensor 2's temp reading as a signed 16-bit value. **LSB =** 0.1 degC";
|
||||
CM_ SG_ 608 RH1 "Sensor 1's % RH value as an unsigned 8-bit value. **LSB =** 1%";
|
||||
CM_ SG_ 608 RH2 "Sensor 2's % RH value as an unsigned 8-bit value. **LSB =** 1%";
|
||||
|
||||
BA_ "GenSigStartValue" SG_ 608 TEMP1 -32768;
|
||||
BA_ "GenSigStartValue" SG_ 608 RH1 255;
|
||||
BA_ "GenSigStartValue" SG_ 608 RH2 254;
|
||||
|
||||
|
||||
There's a lot more that can be done in DBC files like defining Enum values (using ``VAL_``), groups of signals, multiplexed
|
||||
messages (where a messages meaning/Signals change based on the value of one signal), and more. But this covers the basics that I
|
||||
found most helpful while generating code from a DBC file.
|
Loading…
Reference in a new issue