diff --git a/conf.py b/conf.py index 598adc9..0793fd4 100644 --- a/conf.py +++ b/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) diff --git a/container_serve.sh b/container_serve.sh index 84674bb..c9e9579 100755 --- a/container_serve.sh +++ b/container_serve.sh @@ -1,4 +1,4 @@ #!/bin/bash ablog build -python -m http.server -d /website/_website/ 8090 +python -m http.server -d _website/ 8090 diff --git a/pip-requirements.txt b/pip-requirements.txt index 1f144e7..266fae3 100644 --- a/pip-requirements.txt +++ b/pip-requirements.txt @@ -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 diff --git a/posts/canbus_dbc_format.rst b/posts/canbus_dbc_format.rst new file mode 100644 index 0000000..366c1de --- /dev/null +++ b/posts/canbus_dbc_format.rst @@ -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_ : `` + +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_ : |@ (,) [|] "[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_ [ [CAN-ID] [SignalName]] "";`` + +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_] "" [Config];`` + +The data in ``[Config]`` is dependent on the ````. of the attribute being defined. "GenSigStartValue" is a ``INT`` type, so the format of ``[Config]`` will be `` ``. +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 `_ ) + +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.