|
1 | | -# CAN Lecture RX Firmware — Modular Architecture (with Message Router) |
| 1 | +# CAN Bus Demo with ESP32, LVGL, and DBC Code Generation |
2 | 2 |
|
3 | | -This README describes the implemented RX firmware architecture. It replaces the earlier refactor plan with the current, working design that introduces a MessageRouter for modular fan-out of CAN messages and a dedicated IO relay module for blinkers. |
| 3 | +A demonstration project showcasing **CAN bus communication** between two ESP32 boards with **LVGL GUI**, **DBC-based code generation**, and a **modular firmware architecture**. This project is ideal for learning CAN protocols, embedded UI development, and code generation workflows. |
4 | 4 |
|
5 | | -## High-level data flow |
| 5 | +--- |
6 | 6 |
|
7 | | -1) CAN ISR (esp32_can) → EventQueue |
8 | | - - `CanInterface` sets up pins, bitrate, and mailbox filtering for `Cluster` message (ID 0x65). |
9 | | - - ISR validates ID/DLC/IDE and unpacks the frame into `Cluster_t` (generated from DBC), then pushes `Event::ClusterFrame` to a FreeRTOS queue (`EventQueue`). |
| 7 | +## 🎯 What This Project Demonstrates |
10 | 8 |
|
11 | | -2) SystemController (state machine) → MessageRouter |
12 | | - - `SystemController::Dispatch(EventType::ClusterFrame)` handles state transitions (`WaitingForData` → `Active`, recovery from `Degraded`). |
13 | | - - It unconditionally publishes the `Cluster_t` to the `MessageRouter` with a millisecond timestamp. |
| 9 | +1. **CAN Bus Communication** |
| 10 | + - Dual ESP32 setup: one **TX board** (transmitter) and one **RX board** (receiver) |
| 11 | + - 500 kbps CAN communication using ESP32's built-in CAN controller |
| 12 | + - Message filtering, unpacking, and event-driven processing |
14 | 13 |
|
15 | | -3) MessageRouter (central pub/sub) |
16 | | - - Typed topic for `Cluster_t` with subscribe/unsubscribe and a sticky last-value cache. |
17 | | - - Maintains a last-seen timestamp for freshness checks. |
| 14 | +2. **DBC-Driven Code Generation** |
| 15 | + - Define CAN messages in a standard `.dbc` file |
| 16 | + - Auto-generate C code for encoding/decoding using `c-coderdbc` |
| 17 | + - Type-safe message handling with generated structs |
18 | 18 |
|
19 | | -4) Subscribers consume from the router |
20 | | - - `UiController`: subscribed via a small adapter, maps `Cluster_t` to `UiData` and enqueues it to the UI task (LVGL thread). |
21 | | - - `IOModule` (blinkers): subscribed; drives two relay GPIOs for left/right indicators with its own blink state machine (independent of message rate). |
22 | | - - `HealthMonitor`: pull model; no longer notified by subscribers. It queries the router’s last-seen timestamp to detect staleness and emits `FrameTimeout` events. |
| 19 | +3. **LVGL Graphical UI (RX Board)** |
| 20 | + - Real-time display of CAN data on TFT touchscreen |
| 21 | + - SquareLine Studio integration for UI design |
| 22 | + - Arc gauge for speed, turn signal indicators |
23 | 23 |
|
24 | | -5) SystemController::Update() |
25 | | - - Periodically asks `HealthMonitor` to check for timeouts (pulling timestamps from the router) and transitions to `Degraded` on staleness. |
| 24 | +4. **Modular Firmware Architecture** |
| 25 | + - Event-driven state machine with message routing |
| 26 | + - Pub/sub pattern for decoupled components |
| 27 | + - Health monitoring with automatic degradation and recovery |
26 | 28 |
|
27 | | -## State machine |
| 29 | +--- |
28 | 30 |
|
29 | | -`SystemState` lifecycle is unchanged but simplified by the router: |
| 31 | +## 🚀 Quick Start |
30 | 32 |
|
31 | | -- Boot → DisplayInit → WaitingForData → Active |
32 | | -- Active ⇄ Degraded on `FrameTimeout` and recovery when frames resume |
33 | | -- Fault for unrecoverable errors |
| 33 | +### Prerequisites |
34 | 34 |
|
35 | | -`SystemController` still owns transitions and UI screen changes; modules subscribe to data via the router rather than being hard-wired in the controller. |
| 35 | +1. **Hardware** |
| 36 | + - 2x ESP32 boards (e.g., ESP32-EVB or similar) |
| 37 | + - CAN transceivers (e.g., SN65HVD230 or MCP2551) |
| 38 | + - TFT display with touch (for RX board) |
| 39 | + - CAN bus wiring (120Ω termination resistors recommended) |
36 | 40 |
|
37 | | -## Modules (responsibilities and locations) |
| 41 | +2. **Software** |
| 42 | + - [PlatformIO](https://platformio.org/) (VS Code extension or CLI) |
| 43 | + - [Python 3.x](https://www.python.org/) (for optional documentation builds) |
| 44 | + - Git (to clone this repository) |
38 | 45 |
|
39 | | -- CanInterface (`src/rx/CanInterface.{h,cpp}`) |
40 | | - - Configures CAN and registers ISR. |
41 | | - - Unpacks `Cluster_t` in ISR and pushes `Event::ClusterFrame` to `EventQueue`. |
| 46 | +### Installation |
42 | 47 |
|
43 | | -- EventQueue (`src/rx/EventQueue.{h,cpp}`) |
44 | | - - FreeRTOS queue wrapper for typed events from ISR and other producers. |
| 48 | +1. **Clone the repository** |
| 49 | + ```bash |
| 50 | + git clone https://github.com/rvxfahim/CAN-Demo-ESP32.git |
| 51 | + cd CAN-Demo-ESP32 |
| 52 | + ``` |
45 | 53 |
|
46 | | -- MessageRouter (`src/common/MessageRouter.{h,cpp}`) |
47 | | - - Typed subscription API: `SubscribeCluster`, `PublishCluster`. |
48 | | - - Sticky last-value cache and `GetLastSeenMs()` for freshness. |
49 | | - - All subscribers run in task context (publish is called from main loop via controller dispatch). |
| 54 | +2. **Configure serial ports** (if different from defaults) |
| 55 | + Edit `platformio.ini` and set your COM ports: |
| 56 | + ```ini |
| 57 | + [env:rx_board] |
| 58 | + upload_port = COM8 ; Change to your RX board port |
| 59 | + monitor_port = COM8 |
| 60 | + |
| 61 | + [env:tx_board] |
| 62 | + upload_port = COM9 ; Change to your TX board port |
| 63 | + monitor_port = COM9 |
| 64 | + ``` |
50 | 65 |
|
51 | | -- UiController (`src/rx/UiController.{h,cpp}`) |
52 | | - - Initializes LVGL, TFT_eSPI, touch, and SquareLine-generated UI. |
53 | | - - Dedicated UI task with queues; subscribed to router for `Cluster` updates (via adapter in `main.cpp`). |
| 66 | +3. **Build and upload** |
| 67 | + |
| 68 | + **RX Board** (receiver with display): |
| 69 | + ```bash |
| 70 | + pio run -e rx_board -t upload |
| 71 | + pio device monitor -e rx_board |
| 72 | + ``` |
| 73 | + |
| 74 | + **TX Board** (transmitter): |
| 75 | + ```bash |
| 76 | + pio run -e tx_board -t upload |
| 77 | + pio device monitor -e tx_board |
| 78 | + ``` |
54 | 79 |
|
55 | | -- IOModule (`src/rx/IOModule.{h,cpp}`) |
56 | | - - Drives two relay outputs for left/right turn indicators. |
57 | | - - Subscribes to `Cluster` via router. |
58 | | - - Internal blink state machine at 1 Hz (500 ms ON / 500 ms OFF), decoupled from message rate. |
59 | | - - Safe default OFF if no update for >1s. |
60 | | - - Relay pins configurable in `include/IOPins.h` (defaults: GPIO 25 and 26). |
| 80 | +4. **Connect the hardware** |
| 81 | + - Wire CAN_H and CAN_L between boards through transceivers |
| 82 | + - Connect 120Ω termination resistors at both ends |
| 83 | + - Power both boards |
| 84 | + - TX will start sending `Cluster` frames; RX display will update |
61 | 85 |
|
62 | | -- HealthMonitor (`src/rx/HealthMonitor.{h,cpp}`) |
63 | | - - Pull model for staleness: `CheckTimeout(EventQueue&, const MessageRouter&)` reads `GetLastSeenMs()` and emits `FrameTimeout` if stale (default 1500 ms, configurable). |
| 86 | +--- |
64 | 87 |
|
65 | | -- SystemController (`src/rx/SystemController.{h,cpp}`) |
66 | | - - Orchestrates boot, display init, waiting/active/degraded/fault. |
67 | | - - Publishes `Cluster_t` to router on each frame event and handles state transitions. |
| 88 | +## 📖 Understanding the DBC Workflow |
68 | 89 |
|
69 | | -- Entry points (`src/rx/main.cpp`) |
70 | | - - Instantiates modules and runs the boot sequence. |
71 | | - - Subscribes UI to router; starts IO module subscription. |
72 | | - - Calls `ioModule.Update(millis())` for blink timing and `systemController.Update()` for health. |
| 90 | +### What is a DBC File? |
73 | 91 |
|
74 | | -## Message formats and DBC integration |
| 92 | +A **DBC** (CAN Database) file is an industry-standard format for defining CAN messages, signals, and their properties. It serves as the **single source of truth** for all CAN communication in this project. |
75 | 93 |
|
76 | | -- Source of truth: `tools/Lecture.dbc`. |
77 | | -- Code generation: `lib/Generated/lib/lecture.{c,h}` (do not edit generated files). |
78 | | -- Wrapper: `src/generated_lecture_dbc.c` includes generated code; use `Cluster_t`, `Pack_Cluster_lecture`, `Unpack_Cluster_lecture`. |
| 94 | +**Location:** `tools/Lecture.dbc` |
79 | 95 |
|
80 | | -## Extending the system |
| 96 | +### Code Generation Process |
81 | 97 |
|
82 | | -- To add a new consumer of `Cluster` data, subscribe via `MessageRouter::SubscribeCluster(cb, ctx)` and process updates in task context. |
83 | | -- For additional CAN messages/IDs: |
84 | | - - Extend `CanInterface` to push new events. |
85 | | - - Add typed topics (similar to `Cluster`) in `MessageRouter` or expose per-ID raw subscriptions. |
86 | | - - Have modules subscribe to those topics rather than plumbing through `SystemController`. |
| 98 | +This project uses **c-coderdbc** to auto-generate C encode/decode functions from the DBC file. |
87 | 99 |
|
88 | | -## Build, upload, monitor (PlatformIO) |
| 100 | +#### Step 1: Edit the DBC File |
89 | 101 |
|
90 | | -- Build RX: `pio run -e rx_board` |
91 | | -- Upload RX: `pio run -e rx_board -t upload` |
92 | | -- Monitor RX: `pio device monitor -e rx_board` |
| 102 | +The example `Lecture.dbc` defines a `Cluster` message (ID 0x65) with signals: |
| 103 | +- `Speed` (12-bit): 0–4095 range |
| 104 | +- `Left_Turn_Signal` (1-bit): boolean |
| 105 | +- `Right_Turn_Signal` (1-bit): boolean |
93 | 106 |
|
94 | | -Serial ports and speeds are set in `platformio.ini` per environment. |
| 107 | +You can edit this file with any text editor or use tools like [CANdb++ Editor](https://vector.com/) or [SavvyCAN](https://www.savvycan.com/). |
95 | 108 |
|
96 | | -## Notable implementation details |
| 109 | +#### Step 2: Run the Code Generator |
97 | 110 |
|
98 | | -- ISR is minimal and never calls subscribers; it only validates, unpacks, and queues events. |
99 | | -- Router timestamps are in milliseconds (`millis()`), providing a single source of truth for freshness. |
100 | | -- UI and IO operate fully from router data; controller no longer pushes UI payloads directly. |
101 | | -- Blinkers are immune to CAN update rate; they align phase on rising edges and toggle at a fixed cadence. |
| 111 | +**Windows:** |
| 112 | +```powershell |
| 113 | +cd tools\c-coderdbc |
| 114 | +.\build\coderdbc.exe -dbc ..\Lecture.dbc -out ..\..\lib\Generated |
| 115 | +``` |
102 | 116 |
|
103 | | -## Troubleshooting |
| 117 | +This regenerates: |
| 118 | +- `lib/Generated/lib/lecture.c` and `lecture.h` (generated code, **do not edit**) |
| 119 | +- Helper files in `lib/Generated/conf/`, `lib/Generated/inc/`, etc. |
104 | 120 |
|
105 | | -- If the UI doesn’t update: ensure UI task started before router subscription (it is subscribed after `RunBootSequence()` in `main.cpp`). |
106 | | -- If blinkers don’t actuate: verify relay pins in `include/IOPins.h` match hardware; confirm `Cluster.Left_Turn_Signal`/`Right_Turn_Signal` bits from TX. |
107 | | -- If system drops to Degraded: router last-seen likely stale; check TX is sending `Cluster` frames and bus wiring. |
| 121 | +**Linux/macOS:** |
| 122 | +You'll need to compile `c-coderdbc` from source (see `tools/c-coderdbc/README.md`). |
108 | 123 |
|
109 | | -## References |
| 124 | +#### Step 3: Use Generated Types in Code |
110 | 125 |
|
111 | | -- LVGL v9.1 |
112 | | -- esp32_can (custom wrapper in `lib/CanDriver`) |
113 | | -- DBC generator under `tools/c-coderdbc/` |
| 126 | +The firmware uses the generated types exclusively: |
| 127 | + |
| 128 | +```cpp |
| 129 | +#include "lecture.h" // Generated header |
| 130 | + |
| 131 | +// Packing (TX side) |
| 132 | +Cluster_t cluster = {0}; |
| 133 | +cluster.Speed = 2048; |
| 134 | +cluster.Left_Turn_Signal = 1; |
| 135 | +cluster.Right_Turn_Signal = 0; |
| 136 | + |
| 137 | +uint8_t data[8]; |
| 138 | +Pack_Cluster_lecture(&cluster, data, 8); |
| 139 | +CAN0.sendFrame({ .identifier = 0x65, .data = data, ... }); |
| 140 | + |
| 141 | +// Unpacking (RX side) |
| 142 | +Cluster_t received; |
| 143 | +Unpack_Cluster_lecture(&received, frame.data, frame.data_length_code); |
| 144 | +``` |
| 145 | +
|
| 146 | +**Key principle:** Never manually parse CAN bytes. Always use `Pack_*` and `Unpack_*` functions. |
| 147 | +
|
| 148 | +--- |
| 149 | +
|
| 150 | +## 🏗️ Project Structure |
| 151 | +
|
| 152 | +``` |
| 153 | +CAN-Demo-ESP32/ |
| 154 | +├── platformio.ini # Build config (two environments: rx_board, tx_board) |
| 155 | +├── tools/ |
| 156 | +│ ├── Lecture.dbc # CAN message definitions (source of truth) |
| 157 | +│ └── c-coderdbc/ # DBC-to-C code generator |
| 158 | +├── lib/ |
| 159 | +│ ├── Generated/ # Auto-generated C code (from DBC) |
| 160 | +│ ├── CanDriver/ # ESP32 CAN driver abstraction |
| 161 | +│ ├── Ui/ # LVGL UI (SquareLine Studio exports) |
| 162 | +│ └── TouchLibrary/ # Touch controller drivers |
| 163 | +├── src/ |
| 164 | +│ ├── common/ # Shared code (MessageRouter, etc.) |
| 165 | +│ ├── rx/ # RX board firmware (main + modules) |
| 166 | +│ ├── tx/ # TX board firmware (main only) |
| 167 | +│ └── generated_lecture_dbc.c # Single include wrapper for DBC code |
| 168 | +├── include/ # Global headers (IOPins, TFT config) |
| 169 | +├── docs/ # Sphinx documentation source |
| 170 | +└── README.md # This file |
| 171 | +``` |
| 172 | +
|
| 173 | +--- |
| 174 | +
|
| 175 | +## 📚 Documentation |
| 176 | +
|
| 177 | +For **detailed architecture, sequence diagrams, and API references**, see the full documentation: |
| 178 | +
|
| 179 | +### 🌐 [**GitHub Pages Documentation**](https://rvxfahim.github.io/CAN-Demo-ESP32/) |
| 180 | +
|
| 181 | +Topics covered: |
| 182 | +- **Getting Started:** Detailed setup and hardware connections |
| 183 | +- **Architecture:** Component diagrams, state machines, data flow |
| 184 | +- **CAN & DBC:** In-depth DBC workflow and regeneration steps |
| 185 | +- **Testing:** Unit test guidelines and test hooks |
| 186 | +- **API Reference:** Doxygen-generated class/function documentation |
| 187 | +
|
| 188 | +--- |
| 189 | +
|
| 190 | +## 🔧 Configuration and Customization |
| 191 | +
|
| 192 | +### CAN Parameters |
| 193 | +- **Bitrate:** 500 kbps (configurable in `CanInterface.cpp`) |
| 194 | +- **Pins:** GPIO 35 (RX), GPIO 5 (TX) — change in `CanInterface::Initialize()` |
| 195 | +- **Message ID:** `0x65` for `Cluster` (defined in DBC) |
| 196 | +
|
| 197 | +### Display and UI |
| 198 | +- **Screen:** Configured in `include/TFTConfiguration.h` and `lib/Ui/` |
| 199 | +- **UI Design:** Edit with [SquareLine Studio](https://squareline.io/), export to `lib/Ui/` |
| 200 | +- **Widgets:** Arc gauge (`ui_Arc1`), labels (`ui_RightLabel`, `ui_LeftLabel`) |
| 201 | +
|
| 202 | +### IO Relays (Blinkers) |
| 203 | +- **Pins:** GPIO 25 (left), GPIO 26 (right) — see `include/IOPins.h` |
| 204 | +- **Blink Rate:** 1 Hz (500ms ON/OFF) — adjust in `IOModule::Update()` |
| 205 | +
|
| 206 | +### Health Monitoring |
| 207 | +- **Timeout:** 1500ms (RX declares `Degraded` if no CAN frames) — see `HealthMonitor.cpp` |
| 208 | +
|
| 209 | +--- |
| 210 | +
|
| 211 | +## 🧪 Testing and Troubleshooting |
| 212 | +
|
| 213 | +### Common Issues |
| 214 | +
|
| 215 | +**RX display doesn't update:** |
| 216 | +- Verify TX is sending frames (check TX serial monitor) |
| 217 | +- Check CAN wiring and termination resistors |
| 218 | +- Ensure both boards have matching bitrate (500 kbps) |
| 219 | +
|
| 220 | +**Blinkers don't work:** |
| 221 | +- Confirm relay pins in `include/IOPins.h` match your hardware |
| 222 | +- Verify `Left_Turn_Signal` / `Right_Turn_Signal` bits are set in TX |
| 223 | +
|
| 224 | +**System shows "Degraded":** |
| 225 | +- Normal behavior when CAN frames stop arriving (timeout threshold) |
| 226 | +- Should auto-recover when TX resumes |
| 227 | +
|
| 228 | +### Build Errors |
| 229 | +
|
| 230 | +**Missing LVGL or TFT_eSPI:** |
| 231 | +PlatformIO will auto-install dependencies from `lib_deps` in `platformio.ini`. |
| 232 | +
|
| 233 | +**Linker errors about `Cluster_t`:** |
| 234 | +Ensure `src/generated_lecture_dbc.c` is included in the build (check `build_src_filter`). |
| 235 | +
|
| 236 | +--- |
| 237 | +
|
| 238 | +## 🤝 Contributing |
| 239 | +
|
| 240 | +Contributions welcome! Please: |
| 241 | +1. Fork the repository |
| 242 | +2. Create a feature branch (`git checkout -b feature/amazing-feature`) |
| 243 | +3. Commit changes (`git commit -m 'Add amazing feature'`) |
| 244 | +4. Push to branch (`git push origin feature/amazing-feature`) |
| 245 | +5. Open a Pull Request |
| 246 | +
|
| 247 | +--- |
| 248 | +
|
| 249 | +## 📄 License |
| 250 | +
|
| 251 | +This project is licensed under the MIT License. See `LICENSE` file for details. |
| 252 | +
|
| 253 | +--- |
| 254 | +
|
| 255 | +## 🔗 Key Technologies |
| 256 | +
|
| 257 | +- **[PlatformIO](https://platformio.org/)** — Cross-platform embedded build system |
| 258 | +- **[LVGL v9.1](https://lvgl.io/)** — Lightweight graphics library |
| 259 | +- **[TFT_eSPI](https://github.com/Bodmer/TFT_eSPI)** — Fast TFT display driver |
| 260 | +- **[SquareLine Studio](https://squareline.io/)** — Drag-and-drop LVGL UI designer |
| 261 | +- **[c-coderdbc](https://github.com/astand/c-code-generator-from-dbc)** — DBC-to-C code generator |
| 262 | +- **ESP32 Arduino** — Arduino framework for ESP32 |
| 263 | +
|
| 264 | +--- |
| 265 | +
|
| 266 | +## 📞 Questions or Support |
| 267 | +
|
| 268 | +- **Documentation:** https://rvxfahim.github.io/CAN-Demo-ESP32/ |
| 269 | +- **Issues:** [GitHub Issues](https://github.com/rvxfahim/CAN-Demo-ESP32/issues) |
| 270 | +- **Repository:** [github.com/rvxfahim/CAN-Demo-ESP32](https://github.com/rvxfahim/CAN-Demo-ESP32) |
0 commit comments