From 0997716e6b883190219481006fb68feaeb09b282 Mon Sep 17 00:00:00 2001 From: ducky64 Date: Tue, 21 Feb 2017 02:05:57 -0800 Subject: [PATCH 01/26] can intro --- README.md | 6 ++++++ lab2.md | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 lab2.md diff --git a/README.md b/README.md index feaefcc..1a1decb 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,8 @@ # braintrain BRAINv3-based firmware training + +Labs currently available: +- [lab1.md](Lab 1: Embedded Introduction) + +Coming soon: +- [lab2.md](Lab 2: CAN Bus and Multitasking) diff --git a/lab2.md b/lab2.md new file mode 100644 index 0000000..9266c3c --- /dev/null +++ b/lab2.md @@ -0,0 +1,48 @@ +# lab2: CAN Bus and Multitasking + +## Objectives +Objectives +- Learn the mbed CAN API and the basic underlying principles/ +- Learn about and use the different multitasking models on a CAN node. + +## Introduction +[CAN (Controller Area Network)](https://en.wikipedia.org/wiki/CAN_bus) is a communications standard used to network electronics in vehicles, including our solar car. While commercial vehicles also have other network standards to choose from (and some may use a combination of several), all the networked devices (including battery management system, motor controller, telemetry, MPPT) on our car communicate over a shared CAN bus. + +### Interface Level +A CAN bus consists of at least two devices (nodes) connected to a shared bus, and is a _message-based_, _broadcast_ network: +- Message-based: transmissions occur in complete messages (also called frames). +- Broadcast: all nodes on the network can hear the transmissions of any node (though they need not act on irrelevant messages). + +### Message Format +CAN messages carry these pieces of user data: +- ID field, either 11 bits (standard frame) or 29 bits (extended frame). A lower ID gives the message priority in the event of a collision (multiple nodes start transmitting simultaneously). +- Data, up to 8 bytes. The length is transmitted as part of the frame. +- Remote transmission request (RTR), one bit, which marks this as a remote request frame, as opposed to a data frame. + +The ID field is used to indicate the kind of data being transmitted (for example, "battery voltage") and the data contains the actual data (continuing the example, 107.4 V encoded as 2 bytes fixed point). Message IDs must be unique on any single CAN bus. Because of the priority system, giving a lower ID to more important messages (or messages with stricter deadlines) is recommended. + +Usually, nodes transmit data regularly (for example, battery voltage is automatically sent every second by the BMS), but the RTR field can be used to request specific data. Acting on RTR messages must be handled by application code, and it's not something we use. + +CAN also provides these non-user data fields: +- [CRC](https://en.wikipedia.org/wiki/Cyclic_redundancy_check), 15 bits, as a checksum so corrupted messages are discarded. +- Acknowledgement, one bit, asserted by any other node upon successful reception (including CRC check). + - If a message is not acknowledged by any other node, the transmitter will retransmit it. + - However, it is impossible to tell from the acknowledgement bit if a message was received by a particular node. + - As acknowledgements are generated by the CAN controller peripheral, before higher-level application processing. + +### Errors +CAN controllers keep count of errors, and too many errors may result in a _bus-off_ condition, where the controller disconnects from the bus. The CAN controller must be re-initialized (through user-level code) before communications can resume. The full list of error counter rules is complex and a more detailed explanation is [here](https://www.kvaser.com/about-can/the-can-protocol/can-error-handling/). + +### Physical Layer + + +## Lab 2.1: Getting Started: Hardware +Hook up a pin to + +Note: the example solutions use P0_28 for RXD and P0_29 for TXD. + +## Lab 2.2: Sending CAN Messages +Transmit a CAN message on a button press, which pulses a remote LED. Sketch for edge detection also provided. + +## Lab 2.3: Receiving CAN Messages +Receive a message with a particular ID which sets the From 936e37d0e41116b915b614948e1280cea8c7efb3 Mon Sep 17 00:00:00 2001 From: ducky64 Date: Tue, 21 Feb 2017 02:15:51 -0800 Subject: [PATCH 02/26] CAN PHY overview --- lab2.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lab2.md b/lab2.md index 9266c3c..fbb838f 100644 --- a/lab2.md +++ b/lab2.md @@ -21,10 +21,10 @@ CAN messages carry these pieces of user data: The ID field is used to indicate the kind of data being transmitted (for example, "battery voltage") and the data contains the actual data (continuing the example, 107.4 V encoded as 2 bytes fixed point). Message IDs must be unique on any single CAN bus. Because of the priority system, giving a lower ID to more important messages (or messages with stricter deadlines) is recommended. -Usually, nodes transmit data regularly (for example, battery voltage is automatically sent every second by the BMS), but the RTR field can be used to request specific data. Acting on RTR messages must be handled by application code, and it's not something we use. +Usually, nodes transmit data regularly (for example, battery voltage is automatically sent every second by the BMS), but the RTR field can be used to request specific data. Responding to RTR messages must be handled by application code, and it's not something we use. CAN also provides these non-user data fields: -- [CRC](https://en.wikipedia.org/wiki/Cyclic_redundancy_check), 15 bits, as a checksum so corrupted messages are discarded. +- [Cyclic Redundancy Check (CRC)](https://en.wikipedia.org/wiki/Cyclic_redundancy_check), 15 bits, as a checksum so corrupted messages are discarded. - Acknowledgement, one bit, asserted by any other node upon successful reception (including CRC check). - If a message is not acknowledged by any other node, the transmitter will retransmit it. - However, it is impossible to tell from the acknowledgement bit if a message was received by a particular node. @@ -34,7 +34,13 @@ CAN also provides these non-user data fields: CAN controllers keep count of errors, and too many errors may result in a _bus-off_ condition, where the controller disconnects from the bus. The CAN controller must be re-initialized (through user-level code) before communications can resume. The full list of error counter rules is complex and a more detailed explanation is [here](https://www.kvaser.com/about-can/the-can-protocol/can-error-handling/). ### Physical Layer +_This is only a very high-level overview._ +The CAN bus itself consists of two wires, CANH and CANL as a differential pair, to which all nodes are attached. There are two bit levels, either dominant (0) or recessive (1). When multiple nodes transmit simultaneously (collision), a dominant bit takes priority over a recessive bit (the mechanism for ID-based arbitration). If no node is transmitting, the bus is held at a recessive level. + +The CAN controller operates on two different, single-ended (non-differential), logic-level lines: TXD and RXD. RXD indicates the current bit level on the CANH/CANL lines, while TXD indicates the bit to transmit. + +A CAN transceiver bridges the logic-level TXD/RXD lines and the bus-level CANH/CANL lines. While CAN controllers may be a on-chip peripheral on some microcontrollers, CAN transceivers are usually separate chips and may provide some degree of electrical isolation. ## Lab 2.1: Getting Started: Hardware Hook up a pin to From 42c17a17a22feb24c457a396045d5df1844bc0f0 Mon Sep 17 00:00:00 2001 From: ducky64 Date: Tue, 21 Feb 2017 02:16:49 -0800 Subject: [PATCH 03/26] unreverse links --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1a1decb..1cc4745 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ BRAINv3-based firmware training Labs currently available: -- [lab1.md](Lab 1: Embedded Introduction) +- [Lab 1: Embedded Introduction](lab1.md) Coming soon: -- [lab2.md](Lab 2: CAN Bus and Multitasking) +- [Lab 2: CAN Bus and Multitasking](lab2.md) From 206d41f76c234320b9e14462209fcff7b40d6ba1 Mon Sep 17 00:00:00 2001 From: ducky64 Date: Tue, 21 Feb 2017 02:21:22 -0800 Subject: [PATCH 04/26] lab sketch --- lab2.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lab2.md b/lab2.md index fbb838f..8d122b8 100644 --- a/lab2.md +++ b/lab2.md @@ -43,7 +43,10 @@ The CAN controller operates on two different, single-ended (non-differential), l A CAN transceiver bridges the logic-level TXD/RXD lines and the bus-level CANH/CANL lines. While CAN controllers may be a on-chip peripheral on some microcontrollers, CAN transceivers are usually separate chips and may provide some degree of electrical isolation. ## Lab 2.1: Getting Started: Hardware -Hook up a pin to +Connect BRAINv3.3 to CAN transceiver board. +Connect transceiver to class bus with pre-programmed master node. If working alone, there should be a bus-on-a-breadboard in storage with Lab1.4 implemented so you have something to behaviorally check your work against. + +TODO: ckt diagram Note: the example solutions use P0_28 for RXD and P0_29 for TXD. @@ -51,4 +54,10 @@ Note: the example solutions use P0_28 for RXD and P0_29 for TXD. Transmit a CAN message on a button press, which pulses a remote LED. Sketch for edge detection also provided. ## Lab 2.3: Receiving CAN Messages -Receive a message with a particular ID which sets the +Receive a regularly-sent message with a particular ID which sets the RGB LED hue and intensity, allowing all the bus lights to be synchronized. + +## Extra for Experts Lab 1.4: Cooperative Multitasking +Pulse a LED on for a second (or so) when a message is received, while doing all of Lab 2.2 and 2.3, using timer-based polling + +## Extra for Experts Lab 1.5: Threaded Multitasking +Pulse a LED on for a second (or so) when a message is received, while doing all of Lab 2.2 and 2.3, using mbed RTOS From b39b3a48b0a2f49bd5263655eaa3ec2d6f066c38 Mon Sep 17 00:00:00 2001 From: ducky64 Date: Tue, 21 Feb 2017 02:27:35 -0800 Subject: [PATCH 05/26] rewording --- lab2.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lab2.md b/lab2.md index 8d122b8..7efc169 100644 --- a/lab2.md +++ b/lab2.md @@ -13,22 +13,24 @@ A CAN bus consists of at least two devices (nodes) connected to a shared bus, an - Message-based: transmissions occur in complete messages (also called frames). - Broadcast: all nodes on the network can hear the transmissions of any node (though they need not act on irrelevant messages). +The two main operations are transmitting and receiving messages. + ### Message Format CAN messages carry these pieces of user data: -- ID field, either 11 bits (standard frame) or 29 bits (extended frame). A lower ID gives the message priority in the event of a collision (multiple nodes start transmitting simultaneously). +- ID field, either 11 bits (standard frame) or 29 bits (extended frame). A lower ID gives the message priority in the event of a collision (which happens when multiple nodes start transmitting simultaneously). - Data, up to 8 bytes. The length is transmitted as part of the frame. -- Remote transmission request (RTR), one bit, which marks this as a remote request frame, as opposed to a data frame. +- Remote transmission request (RTR), one bit. This differentiates a remote request frame from a data frame, and more details are below. -The ID field is used to indicate the kind of data being transmitted (for example, "battery voltage") and the data contains the actual data (continuing the example, 107.4 V encoded as 2 bytes fixed point). Message IDs must be unique on any single CAN bus. Because of the priority system, giving a lower ID to more important messages (or messages with stricter deadlines) is recommended. +The ID field is used to indicate the kind of data being transmitted (for example, "battery voltage") and the data field contains the actual data (continuing the example, 107.4 V encoded as 2 bytes fixed point). Message IDs must be unique on any single CAN bus. Because of the priority system, giving a lower ID to more important messages (or messages with stricter deadlines) is recommended. Usually, nodes transmit data regularly (for example, battery voltage is automatically sent every second by the BMS), but the RTR field can be used to request specific data. Responding to RTR messages must be handled by application code, and it's not something we use. CAN also provides these non-user data fields: - [Cyclic Redundancy Check (CRC)](https://en.wikipedia.org/wiki/Cyclic_redundancy_check), 15 bits, as a checksum so corrupted messages are discarded. - Acknowledgement, one bit, asserted by any other node upon successful reception (including CRC check). - - If a message is not acknowledged by any other node, the transmitter will retransmit it. + - If a message is not acknowledged by any other node, the transmitter will attempt to retransmit. - However, it is impossible to tell from the acknowledgement bit if a message was received by a particular node. - - As acknowledgements are generated by the CAN controller peripheral, before higher-level application processing. + - As acknowledgements are generated by the CAN controller peripheral (before user-level code), it is also impossible to tell if application code has processed and acted on a message correctly. ### Errors CAN controllers keep count of errors, and too many errors may result in a _bus-off_ condition, where the controller disconnects from the bus. The CAN controller must be re-initialized (through user-level code) before communications can resume. The full list of error counter rules is complex and a more detailed explanation is [here](https://www.kvaser.com/about-can/the-can-protocol/can-error-handling/). From f177cb3285339f9309bf16831e66a7e5521f44fe Mon Sep 17 00:00:00 2001 From: ducky64 Date: Wed, 22 Feb 2017 02:01:18 -0800 Subject: [PATCH 06/26] Lab 2.2 and partial master node code --- lab2.md | 92 +++++++++++++++++++++++++++++++++++++--- solutions/lab2.2.cpp | 37 ++++++++++++++++ solutions/lab2master.cpp | 48 +++++++++++++++++++++ 3 files changed, 170 insertions(+), 7 deletions(-) create mode 100644 solutions/lab2.2.cpp create mode 100644 solutions/lab2master.cpp diff --git a/lab2.md b/lab2.md index 7efc169..e3070b8 100644 --- a/lab2.md +++ b/lab2.md @@ -45,21 +45,99 @@ The CAN controller operates on two different, single-ended (non-differential), l A CAN transceiver bridges the logic-level TXD/RXD lines and the bus-level CANH/CANL lines. While CAN controllers may be a on-chip peripheral on some microcontrollers, CAN transceivers are usually separate chips and may provide some degree of electrical isolation. ## Lab 2.1: Getting Started: Hardware -Connect BRAINv3.3 to CAN transceiver board. -Connect transceiver to class bus with pre-programmed master node. If working alone, there should be a bus-on-a-breadboard in storage with Lab1.4 implemented so you have something to behaviorally check your work against. +The LPC1549 on the BRAINv3.3 has a CAN controller, but no onboard CAN transceiver. You will need to connect the BRAIN to an external CAN transceiver breakout: connect one BRAIN IO to RXD, another to TXD, and the power supply (Vcc and GND) to the 3.3v microcontroller supply. Connect the CAN side lines (CANH, CANL, Vcc, GND) to the bus signal and supply lines. If working in a class, there should be a pre-programmed master node on the bus. If working alone, there should be a bus-on-a-breadboard in storage with Lab1.4 implemented so you have something to behaviorally check your work against. TODO: ckt diagram -Note: the example solutions use P0_28 for RXD and P0_29 for TXD. +In the example code skeletons, replace `RXD_PIN` and `TXD_PIN` with the appropriate pin name (like `P0_28`). Note that the example solutions use `P0_28` for RXD and `P0_29` for TXD. ## Lab 2.2: Sending CAN Messages -Transmit a CAN message on a button press, which pulses a remote LED. Sketch for edge detection also provided. +The master node is configured to pulse its white LED on for 0.25 seconds when receiving a CAN message with ID 0x42. -## Lab 2.3: Receiving CAN Messages +**Objective**: When the button is pressed on your BRAIN, have it transmit a message that triggers a blink on the remote master node. + +Start with this code skeleton: + +```c++ +#include "mbed.h" + +#include "ledutils.h" + +PwmOut ledR(P0_5); +PwmOut ledG(P0_6); +PwmOut ledB(P0_7); + +DigitalOut led1(P0_3); +DigitalOut led2(P0_9); + +DigitalIn btn(P0_4); + +RawSerial serial(P0_8, NC, 115200); + +CAN can(RXD_PIN, TXD_PIN); + +int main() { + // Start with RGB LED off + ledR = 1; + ledG = 1; + ledB = 1; + + // Initialize CAN controller at 1 Mbaud + can.frequency(1000000); + + while (true) { + /* YOUR CODE HERE */ + } +} +``` + +_Remember to replace `RXD_PIN` and `TXD_PIN` with the appropriate pins based on your hardware configuration. Objects declarations for the on-board hardware (LEDs, switches, and a serial console), have been provided for your convenience._ + +### Detecting Button Presses + +The first thing you need to do is to detect a button press. Unlike in the previous lab, where the application was only _level-sensitive_ (cares about the state of the button, whether it is pressed or not), this application is _edge-sensitive_ (we will define a button press as the up to down action). The simplest solution is to, in the main loop, track the previous button state and compare the current button state against the previous state. A button press is when the previous button state is `1` (up) and the current button state is `0` (down). Remember to update the previous button state at the end of each loop. + +In keeping with efficient development practices, you may want to test your button press detector in isolation before stacking CAN on top of it. One simple method is to toggle an LED on each press. You may also have it print something to the serial console. + +Problems? Common issues may be: +- Remember to declare your previous button state outside the `while (true)` main loop. Otherwise, it will re-initialize each time around the loop and won't be very useful as a persistent tracker. +- Are you losing edges? Make sure the button state compare and update happen atomically with regards to reading the button state. That is, if you read the button during a compare operation, then read it again to update the previous state, there's no guarantee that both reads return the same result, from the same time. + - A solution around this is to read the button state (once per loop!) into a temporary current state variable, then use that variable in the comparison and update operations. +- Are you detecting multiple edges per press? This is a limitation of mechanical switches: they may bounce for a few milliseconds before they settle. If you sample fast enough, these may register as false edges. + - One solution is to filter in hardware. The most common approach is adding a RC filter. + - Debouncing can also be implemented in firmware. One approach is to wait for the switch signal to settle for some amount of time before changing the current state. This requires additional code (and a very small amount of compute), but no hardware (and hence, no recurring costs). + - For the purposes of this lab, ignore this effect. + +### Writing to CAN + +Compared to the button press, writing to the CAN bus is simple. First, create a message object using the id-only `CANMessage` constructor: + +```c++ +CANMessage msg(0x42); +``` + +Then, transmit that message using `CAN::write(CANMessage)`: + +```c++ +can.write(msg); +``` + +Note that you can combine both operations into one line: + +```c++ +can.write(CANMessage(0x42)); +``` + +Have your edge detection code execute the above on a button press, and you should be done. Feel free to compare against the [solution](solutions/lab2.2.cpp), too. + +## Lab 2.3: CAN Messages with Data + + +## Lab 2.4: Receiving CAN Messages Receive a regularly-sent message with a particular ID which sets the RGB LED hue and intensity, allowing all the bus lights to be synchronized. -## Extra for Experts Lab 1.4: Cooperative Multitasking +## Extra for Experts Lab 2.5: Cooperative Multitasking Pulse a LED on for a second (or so) when a message is received, while doing all of Lab 2.2 and 2.3, using timer-based polling -## Extra for Experts Lab 1.5: Threaded Multitasking +## Extra for Experts Lab 2.6: Threaded Multitasking Pulse a LED on for a second (or so) when a message is received, while doing all of Lab 2.2 and 2.3, using mbed RTOS diff --git a/solutions/lab2.2.cpp b/solutions/lab2.2.cpp new file mode 100644 index 0000000..f47bbb3 --- /dev/null +++ b/solutions/lab2.2.cpp @@ -0,0 +1,37 @@ +#include "mbed.h" + +#include "ledutils.h" + +PwmOut ledR(P0_5); +PwmOut ledG(P0_6); +PwmOut ledB(P0_7); + +DigitalOut led1(P0_3); +DigitalOut led2(P0_9); + +DigitalIn btn(P0_4); + +RawSerial serial(P0_8, NC, 115200); + +CAN can(P0_28, P0_29); + +int main() { + // Start with RGB LED off + ledR = 1; + ledG = 1; + ledB = 1; + + // Initialize CAN controller at 1 Mbaud + can.frequency(1000000); + + // Button edge detection + bool lastButton = true; + + while (true) { + bool thisButton = btn; + if (thisButton != lastButton && btn == 0) { + can.write(CANMessage(0x42)); + } + lastButton = thisButton; + } +} diff --git a/solutions/lab2master.cpp b/solutions/lab2master.cpp new file mode 100644 index 0000000..86b9d90 --- /dev/null +++ b/solutions/lab2master.cpp @@ -0,0 +1,48 @@ +#include "mbed.h" + +#include "ledutils.h" + +PwmOut ledR(P0_5); +PwmOut ledG(P0_6); +PwmOut ledB(P0_7); + +DigitalOut led1(P0_3); +DigitalOut led2(P0_9); + +DigitalIn btn(P0_4); + +RawSerial serial(P0_8, NC, 115200); + +CAN can(P0_28, P0_29); + +int main() { + // Start with RGB LED off + ledR = 1; + ledG = 1; + ledB = 1; + + // LED Blink State + int32_t blinkLengthMs; + Timer ledTimer; // used to track blink length + ledTimer.start(); + + // Initialize CAN controller at 1 Mbaud + can.frequency(1000000); + + while (true) { + // CAN receive handling + CANMessage msg; + while (can.read(msg)) { + if (msg.id == 0x42) { + led2 = 1; + blinkLengthMs = 250; + ledTimer.reset(); + } + } + + // System actions + if (ledTimer.read_ms() >= blinkLengthMs) { + led2 = 0; + } + } +} From b5a2ba0d7d143fad47c97c3fbf1db5f0a9250bb0 Mon Sep 17 00:00:00 2001 From: ducky64 Date: Mon, 27 Feb 2017 01:03:35 -0800 Subject: [PATCH 07/26] Lab2 master debouncing code --- solutions/lab2master.cpp | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/solutions/lab2master.cpp b/solutions/lab2master.cpp index 86b9d90..9f8f05b 100644 --- a/solutions/lab2master.cpp +++ b/solutions/lab2master.cpp @@ -15,6 +15,8 @@ RawSerial serial(P0_8, NC, 115200); CAN can(P0_28, P0_29); +const uint16_t DEBOUNCE_TIME_MS = 50; + int main() { // Start with RGB LED off ledR = 1; @@ -26,6 +28,9 @@ int main() { Timer ledTimer; // used to track blink length ledTimer.start(); + bool lastButton = true; + Timer buttonDebounceTimer; + // Initialize CAN controller at 1 Mbaud can.frequency(1000000); @@ -41,6 +46,26 @@ int main() { } // System actions + bool thisButton = btn; + if (thisButton != lastButton) { + buttonDebounceTimer.start(); + if (buttonDebounceTimer.read_ms() >= DEBOUNCE_TIME_MS) { + // Actual edge occurs here + lastButton = thisButton; + + // Do edge actions + if (thisButton == false) { + can.write(CANMessage(0x42)); + } + } + } + if (thisButton == lastButton) { + // This also gets triggered on a fresh edge. + buttonDebounceTimer.stop(); + buttonDebounceTimer.reset(); + } + + if (ledTimer.read_ms() >= blinkLengthMs) { led2 = 0; } From cf3a87a490ef3d990440514e734d5cc3eb3d9e76 Mon Sep 17 00:00:00 2001 From: ducky64 Date: Sun, 5 Mar 2017 23:29:32 -0800 Subject: [PATCH 08/26] Updated lab2 master with RGB LED and CAN spam --- solutions/lab2master.cpp | 31 +++++++++++++++++++++++++++- src/ledutils.cpp | 44 ++++++++++++++++++++++++++++++++++++++++ src/ledutils.h | 7 +++++++ 3 files changed, 81 insertions(+), 1 deletion(-) diff --git a/solutions/lab2master.cpp b/solutions/lab2master.cpp index 9f8f05b..a671306 100644 --- a/solutions/lab2master.cpp +++ b/solutions/lab2master.cpp @@ -16,6 +16,7 @@ RawSerial serial(P0_8, NC, 115200); CAN can(P0_28, P0_29); const uint16_t DEBOUNCE_TIME_MS = 50; +const uint16_t RGB_LED_UPDATE_MS = 20; // a respectable 50 Hz int main() { // Start with RGB LED off @@ -31,6 +32,14 @@ int main() { bool lastButton = true; Timer buttonDebounceTimer; + ledR.period_us(500); + ledG.period_us(500); + ledB.period_us(500); + + uint16_t hue = 0; + Timer rgbLedTimer; + rgbLedTimer.start(); + // Initialize CAN controller at 1 Mbaud can.frequency(1000000); @@ -65,9 +74,29 @@ int main() { buttonDebounceTimer.reset(); } - if (ledTimer.read_ms() >= blinkLengthMs) { led2 = 0; } + + if (rgbLedTimer.read_ms() >= RGB_LED_UPDATE_MS) { + rgbLedTimer.reset(); + + hue += RGB_LED_UPDATE_MS * 10; + hue = hue % 36000; // 360 degree * 100 + + uint16_t r, g, b; + hsv_to_rgb_pwm_uint16(hue, 65535, 65535, &r, &g, &b); + + // Set outputs. + ledR.pulsewidth_us((uint32_t)r * 500 / 65535); + ledG.pulsewidth_us((uint32_t)g * 500 / 65535); + ledB.pulsewidth_us((uint32_t)b * 500 / 65535); + + // Update CAN with new hue. + uint8_t data[2]; + data[0] = (hue >> 8) & 0xff; + data[1] = (hue >> 0) & 0xff; + can.write(CANMessage(0x43, (char*)data, 3)); + } } } diff --git a/src/ledutils.cpp b/src/ledutils.cpp index 130b051..071930f 100644 --- a/src/ledutils.cpp +++ b/src/ledutils.cpp @@ -52,3 +52,47 @@ void hsv_to_rgb_uint16(uint16_t h_cdeg, uint16_t s, uint16_t v, *g_out = *g_out + m; *b_out = *b_out + m; } + +void hsv_to_rgb_pwm_uint16(uint16_t h_cdeg, uint16_t s, uint16_t v, + uint16_t *r_out, uint16_t *g_out, uint16_t *b_out) { + uint16_t C = (uint32_t)v * s / 65535; + int32_t h_millipct = (((int32_t)h_cdeg * 10 / 60) % 2000) - 1000; // 1000x + uint16_t X = C * (1000 - abs(h_millipct)) / 1000; + uint16_t m = v - C; + + h_cdeg = h_cdeg % 36000; + + uint16_t r, g, b; + if (0 <= h_cdeg && h_cdeg < 6000) { + r = C; g = X; b = 0; + } else if (6000 <= h_cdeg && h_cdeg < 12000) { + r = X; g = C; b = 0; + } else if (12000 <= h_cdeg && h_cdeg < 18000) { + r = 0; g = C; b = X; + } else if (18000 <= h_cdeg && h_cdeg < 24000) { + r = 0; g = X; b = C; + } else if (24000 <= h_cdeg && h_cdeg < 30000) { + r = X; g = 0; b = C; + } else { // 300 <= H < 360 + r = C; g = 0; b = X; + } + + r = r + m; + g = g + m; + b = b + m; + + // Square intensity to account for human perceived brightness. + r = (uint32_t)r * r / 65535; + g = (uint32_t)g * g / 65535; + b = (uint32_t)b * b / 65535; + + // Invert polarity to account for common-anode LED (emits light when pin is low). + r = 65535 - r; + g = 65535 - g; + b = 65535 - b; + + // Write outputs. + *r_out = r; + *g_out = g; + *b_out = b; +} diff --git a/src/ledutils.h b/src/ledutils.h index b6911c1..6b28af8 100644 --- a/src/ledutils.h +++ b/src/ledutils.h @@ -17,4 +17,11 @@ void hsv_to_rgb_float(float h_deg, float s, float v, void hsv_to_rgb_uint16(uint16_t h_cdeg, uint16_t s, uint16_t v, uint16_t *r_out, uint16_t *g_out, uint16_t *b_out); +/* Converts H (centidegrees, [0, 36000)), S, V (in [0, 65535]), + * to R, G, B (all in [0, 65535]) PWM, accounting for polarity inversion and + * human perceived brightness + */ +void hsv_to_rgb_pwm_uint16(uint16_t h_cdeg, uint16_t s, uint16_t v, + uint16_t *r_out, uint16_t *g_out, uint16_t *b_out); + #endif /* LEDUTILS_H_ */ From 9069686ba44a1bf1cfd62de038c97e088bb7a6cb Mon Sep 17 00:00:00 2001 From: ducky64 Date: Sun, 5 Mar 2017 23:42:02 -0800 Subject: [PATCH 09/26] refactor RGB LED --- lab2.md | 9 --------- solutions/lab2.2.cpp | 9 --------- solutions/lab2master.cpp | 17 ++--------------- src/ledutils.cpp | 9 ++++----- src/ledutils.h | 32 +++++++++++++++++++++++++++----- 5 files changed, 33 insertions(+), 43 deletions(-) diff --git a/lab2.md b/lab2.md index e3070b8..cb3a5a3 100644 --- a/lab2.md +++ b/lab2.md @@ -63,10 +63,6 @@ Start with this code skeleton: #include "ledutils.h" -PwmOut ledR(P0_5); -PwmOut ledG(P0_6); -PwmOut ledB(P0_7); - DigitalOut led1(P0_3); DigitalOut led2(P0_9); @@ -77,11 +73,6 @@ RawSerial serial(P0_8, NC, 115200); CAN can(RXD_PIN, TXD_PIN); int main() { - // Start with RGB LED off - ledR = 1; - ledG = 1; - ledB = 1; - // Initialize CAN controller at 1 Mbaud can.frequency(1000000); diff --git a/solutions/lab2.2.cpp b/solutions/lab2.2.cpp index f47bbb3..0632be2 100644 --- a/solutions/lab2.2.cpp +++ b/solutions/lab2.2.cpp @@ -2,10 +2,6 @@ #include "ledutils.h" -PwmOut ledR(P0_5); -PwmOut ledG(P0_6); -PwmOut ledB(P0_7); - DigitalOut led1(P0_3); DigitalOut led2(P0_9); @@ -16,11 +12,6 @@ RawSerial serial(P0_8, NC, 115200); CAN can(P0_28, P0_29); int main() { - // Start with RGB LED off - ledR = 1; - ledG = 1; - ledB = 1; - // Initialize CAN controller at 1 Mbaud can.frequency(1000000); diff --git a/solutions/lab2master.cpp b/solutions/lab2master.cpp index a671306..0d49f9e 100644 --- a/solutions/lab2master.cpp +++ b/solutions/lab2master.cpp @@ -2,9 +2,7 @@ #include "ledutils.h" -PwmOut ledR(P0_5); -PwmOut ledG(P0_6); -PwmOut ledB(P0_7); +RGBPwmOut rgbLed(P0_5, P0_6, P0_7); DigitalOut led1(P0_3); DigitalOut led2(P0_9); @@ -19,11 +17,6 @@ const uint16_t DEBOUNCE_TIME_MS = 50; const uint16_t RGB_LED_UPDATE_MS = 20; // a respectable 50 Hz int main() { - // Start with RGB LED off - ledR = 1; - ledG = 1; - ledB = 1; - // LED Blink State int32_t blinkLengthMs; Timer ledTimer; // used to track blink length @@ -84,13 +77,7 @@ int main() { hue += RGB_LED_UPDATE_MS * 10; hue = hue % 36000; // 360 degree * 100 - uint16_t r, g, b; - hsv_to_rgb_pwm_uint16(hue, 65535, 65535, &r, &g, &b); - - // Set outputs. - ledR.pulsewidth_us((uint32_t)r * 500 / 65535); - ledG.pulsewidth_us((uint32_t)g * 500 / 65535); - ledB.pulsewidth_us((uint32_t)b * 500 / 65535); + rgbLed.hsv_uint16(hue, 65535, 65535); // Update CAN with new hue. uint8_t data[2]; diff --git a/src/ledutils.cpp b/src/ledutils.cpp index 071930f..5dc8a16 100644 --- a/src/ledutils.cpp +++ b/src/ledutils.cpp @@ -53,8 +53,7 @@ void hsv_to_rgb_uint16(uint16_t h_cdeg, uint16_t s, uint16_t v, *b_out = *b_out + m; } -void hsv_to_rgb_pwm_uint16(uint16_t h_cdeg, uint16_t s, uint16_t v, - uint16_t *r_out, uint16_t *g_out, uint16_t *b_out) { +void RGBPwmOut::hsv_uint16(uint16_t h_cdeg, uint16_t s, uint16_t v) { uint16_t C = (uint32_t)v * s / 65535; int32_t h_millipct = (((int32_t)h_cdeg * 10 / 60) % 2000) - 1000; // 1000x uint16_t X = C * (1000 - abs(h_millipct)) / 1000; @@ -92,7 +91,7 @@ void hsv_to_rgb_pwm_uint16(uint16_t h_cdeg, uint16_t s, uint16_t v, b = 65535 - b; // Write outputs. - *r_out = r; - *g_out = g; - *b_out = b; + ledR.pulsewidth_us((uint32_t)r * PWM_PERIOD_US / 65535); + ledG.pulsewidth_us((uint32_t)r * PWM_PERIOD_US / 65535); + ledB.pulsewidth_us((uint32_t)r * PWM_PERIOD_US / 65535); } diff --git a/src/ledutils.h b/src/ledutils.h index 6b28af8..6f02952 100644 --- a/src/ledutils.h +++ b/src/ledutils.h @@ -17,11 +17,33 @@ void hsv_to_rgb_float(float h_deg, float s, float v, void hsv_to_rgb_uint16(uint16_t h_cdeg, uint16_t s, uint16_t v, uint16_t *r_out, uint16_t *g_out, uint16_t *b_out); -/* Converts H (centidegrees, [0, 36000)), S, V (in [0, 65535]), - * to R, G, B (all in [0, 65535]) PWM, accounting for polarity inversion and - * human perceived brightness +/* Common-anode RGB LED controlled by PWM, with fixed-point options. */ -void hsv_to_rgb_pwm_uint16(uint16_t h_cdeg, uint16_t s, uint16_t v, - uint16_t *r_out, uint16_t *g_out, uint16_t *b_out); +class RGBPwmOut { + RGBPwmOut(PinName pinR, PinName pinG, PinName pinB): + ledR(pinR), ledG(pinG), ledB(pinB) { + ledR.period_us(PWM_PERIOD_US); + ledG.period_us(PWM_PERIOD_US); + ledB.period_us(PWM_PERIOD_US); + + // Initialize LEDs to off + ledR.pulsewidth_us(PWM_PERIOD_US); + ledG.pulsewidth_us(PWM_PERIOD_US); + ledB.pulsewidth_us(PWM_PERIOD_US); + } + + /* Writes a HSV value to the RGB LED, accounting for polarity inversion and + * human perceived brightness + * H is in centidegrees, [0, 36000), S, V are in fixed point [0, 65535]. + */ + void hsv_uint16(uint16_t h_cdeg, uint16_t s, uint16_t v); + +protected: + static const uint16_t PWM_PERIOD_US = 500; + + PwmOut ledR; + PwmOut ledG; + PwmOut b; +}; #endif /* LEDUTILS_H_ */ From e60527b5905483a89def7550dd179cfe89683c03 Mon Sep 17 00:00:00 2001 From: ducky64 Date: Sun, 5 Mar 2017 23:54:32 -0800 Subject: [PATCH 10/26] fix RGB LED code --- lab2.md | 2 ++ solutions/lab2.4.cpp | 29 +++++++++++++++++++++++++++++ solutions/lab2master.cpp | 12 ++++++------ src/ledutils.cpp | 4 ++-- src/ledutils.h | 5 ++++- 5 files changed, 43 insertions(+), 9 deletions(-) create mode 100644 solutions/lab2.4.cpp diff --git a/lab2.md b/lab2.md index cb3a5a3..b58c196 100644 --- a/lab2.md +++ b/lab2.md @@ -122,7 +122,9 @@ can.write(CANMessage(0x42)); Have your edge detection code execute the above on a button press, and you should be done. Feel free to compare against the [solution](solutions/lab2.2.cpp), too. ## Lab 2.3: CAN Messages with Data +When the master node receives a CAN message with ID 0x41, it will pulse its white LED with a length specified by the data field, in milliseconds. The blink length is the first 16-bit integer in the payload, in [big-endian (network order)](https://en.wikipedia.org/wiki/Endianness#Networking) format. +**Objective**: When the button is pressed on your BRAIN, have it transmit a message that triggers a blink on the remote master node at some interesting amount of time (not 250ms as the last lab). ## Lab 2.4: Receiving CAN Messages Receive a regularly-sent message with a particular ID which sets the RGB LED hue and intensity, allowing all the bus lights to be synchronized. diff --git a/solutions/lab2.4.cpp b/solutions/lab2.4.cpp new file mode 100644 index 0000000..202425e --- /dev/null +++ b/solutions/lab2.4.cpp @@ -0,0 +1,29 @@ +#include "mbed.h" + +#include "ledutils.h" + +RGBPwmOut rgbLed(P0_5, P0_6, P0_7); + +DigitalOut led1(P0_3); +DigitalOut led2(P0_9); + +DigitalIn btn(P0_4); + +RawSerial serial(P0_8, NC, 115200); + +CAN can(P0_28, P0_29); + +int main() { + // Initialize CAN controller at 1 Mbaud + can.frequency(1000000); + + while (true) { + CANMessage msg; + while (can.read(msg)) { + if (msg.id == 0x43) { + uint16_t hue = (msg.data[0] << 8) | (msg.data[1] << 0); + rgbLed.hsv_uint16(hue, 65535, 65535); + } + } + } +} diff --git a/solutions/lab2master.cpp b/solutions/lab2master.cpp index 0d49f9e..39b8b2d 100644 --- a/solutions/lab2master.cpp +++ b/solutions/lab2master.cpp @@ -18,17 +18,13 @@ const uint16_t RGB_LED_UPDATE_MS = 20; // a respectable 50 Hz int main() { // LED Blink State - int32_t blinkLengthMs; + int32_t blinkLengthMs = 0; Timer ledTimer; // used to track blink length ledTimer.start(); bool lastButton = true; Timer buttonDebounceTimer; - ledR.period_us(500); - ledG.period_us(500); - ledB.period_us(500); - uint16_t hue = 0; Timer rgbLedTimer; rgbLedTimer.start(); @@ -42,8 +38,12 @@ int main() { while (can.read(msg)) { if (msg.id == 0x42) { led2 = 1; + ledTimer.reset(); blinkLengthMs = 250; + } else if (msg.id == 0x41) { + led2 = 1; ledTimer.reset(); + blinkLengthMs = (msg.data[0] << 8) | msg.data[1]; } } @@ -77,7 +77,7 @@ int main() { hue += RGB_LED_UPDATE_MS * 10; hue = hue % 36000; // 360 degree * 100 - rgbLed.hsv_uint16(hue, 65535, 65535); + rgbLed.hsv_uint16(hue, 65535, 32767); // Update CAN with new hue. uint8_t data[2]; diff --git a/src/ledutils.cpp b/src/ledutils.cpp index 5dc8a16..4dde225 100644 --- a/src/ledutils.cpp +++ b/src/ledutils.cpp @@ -92,6 +92,6 @@ void RGBPwmOut::hsv_uint16(uint16_t h_cdeg, uint16_t s, uint16_t v) { // Write outputs. ledR.pulsewidth_us((uint32_t)r * PWM_PERIOD_US / 65535); - ledG.pulsewidth_us((uint32_t)r * PWM_PERIOD_US / 65535); - ledB.pulsewidth_us((uint32_t)r * PWM_PERIOD_US / 65535); + ledG.pulsewidth_us((uint32_t)g * PWM_PERIOD_US / 65535); + ledB.pulsewidth_us((uint32_t)b * PWM_PERIOD_US / 65535); } diff --git a/src/ledutils.h b/src/ledutils.h index 6f02952..0e8f3e1 100644 --- a/src/ledutils.h +++ b/src/ledutils.h @@ -6,6 +6,8 @@ #include +#include "mbed.h" + /* Converts H (degrees, [0, 360)), S, V (in [0, 1]), to R, G, B (all in [0, 1]) */ void hsv_to_rgb_float(float h_deg, float s, float v, @@ -20,6 +22,7 @@ void hsv_to_rgb_uint16(uint16_t h_cdeg, uint16_t s, uint16_t v, /* Common-anode RGB LED controlled by PWM, with fixed-point options. */ class RGBPwmOut { +public: RGBPwmOut(PinName pinR, PinName pinG, PinName pinB): ledR(pinR), ledG(pinG), ledB(pinB) { ledR.period_us(PWM_PERIOD_US); @@ -43,7 +46,7 @@ class RGBPwmOut { PwmOut ledR; PwmOut ledG; - PwmOut b; + PwmOut ledB; }; #endif /* LEDUTILS_H_ */ From e1f5eb6836f01fe0e8156a23dcbbd250e6718420 Mon Sep 17 00:00:00 2001 From: ducky64 Date: Mon, 6 Mar 2017 00:36:06 -0800 Subject: [PATCH 11/26] Rest of lab2 --- lab2.md | 89 +++++++++++++++++++++++++++++++++++++++++++- solutions/lab2.3.cpp | 32 ++++++++++++++++ solutions/lab2.4.cpp | 2 +- 3 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 solutions/lab2.3.cpp diff --git a/lab2.md b/lab2.md index b58c196..0aa9174 100644 --- a/lab2.md +++ b/lab2.md @@ -126,8 +126,95 @@ When the master node receives a CAN message with ID 0x41, it will pulse its whit **Objective**: When the button is pressed on your BRAIN, have it transmit a message that triggers a blink on the remote master node at some interesting amount of time (not 250ms as the last lab). +`CANMessage` has another constructor: + +```c++ +CANMessage(int _id, const char *_data, char _len = 8, CANType _type = CANData, CANFormat _format = CANStandard) +``` + +For the purposes of this lab, you only need to consider the first 3 arguments, which specify the ID, payload, and payload length. Your goal will be to pack a 16-bit integer into the byte-oriented payload field. First, start by declaring the blink length (in this example, 1000 ms = 1 second): + +```c++ +uint16_t blinkLengthMs = 1000; +``` + +Then, declare a 2-byte vector to store the re-packed payload: + +```c++ +uint8_t data[2]; +``` + +Pack the data from `blinkLengthMs` into `data`, one byte at a time. Usually, this is accomplished by shifting the 8 bits to be packed into the leasy significant byte, then masking out the other bytes. For example, to get bits 15...8, we would write: + +```c++ +data[0] = (blinkLengthMs >> 8) & 0xff; +``` + +This right shifts blinkLengthMs by 8, so that bits 15...8 are now in bits 7...0. The masking isn't really necessary for a 16-bit integer, but would be if you were taking a byte in the middle of a larger (32-bit, for example) integer. + +After you've packed `data[1]`, you can construct the CAN message and send it. A convenient one-liner is: + +```c++ +can.write(CANMessage(0x41, (char*)data, 2)); +``` + +This creates a CAN message with id=0x41 and payload with 2 bytes from `data`. Note that the CANMessage API inexplicably takes in a `char*` (rather than an `unsigned char*` or `uint8_t*` type, so the `(char*)` cast is required. + +Run it, press the button, and check that the master pulses its LED for the duration you programmed. A [reference solution](solutions/lab2.3.cpp) is also available for the curious. + ## Lab 2.4: Receiving CAN Messages -Receive a regularly-sent message with a particular ID which sets the RGB LED hue and intensity, allowing all the bus lights to be synchronized. +You may notice that the master node is constantly cycling the hue of its RGB LED. It also broadcasts the RGB LED's hue regularly, allowing other nodes to synchronize with it. + +**Objective**: Have your BRAIN's RGB LED track and mimic the master's RGB LED. + +_Note: you may think another way to accomplish this objective is to run both nodes open-loop, but starting both at the same time. The reason this does not work well is because of clock drift: since each node has its own (non-synchronized) clock source, the timing will be slightly off and their hues will drift. While the crystals on the BRAINs provide good frequency tolerance and stability, they will still de-synchronize over long periods of time._ + +### RGB LED Helper +Start by declaring a RGB LED object. For convenience, a `RGBPwmLed` object has been provided in the `ledutils.h` header. Instantiate one as follows: + +```c++ +RGBPwmOut rgbLed(P0_5, P0_6, P0_7); +``` + +It has one API function, which sets the R, G, B PWM outputs using a input H, S, V. The hue is specified in centi-degrees [0, 36000), while the saturation and value are specified in 16-bit fixed point [0, 65535]. + +```c++ +RGBPwmOut::hsv_uint16(uint16_t h_cdeg, uint16_t s, uint16_t v); +``` + +### Reading from CAN +Messages can be read from a CAN object using `CAN::read(CANMessage&)`. If there was a message pending, the function returns 1 and stores the message in the input (reference) argument. If there was no message pending, the function return 0. + +> In lab1, you learned about pointers. C++ also has _reference_ types, denoted with `&`, which act like pointers but use value notation. You can see its use in `CAN::read`, allowing the function to return both a read status (as its return value) and a message. +> +> Note: some style guides discourage the use of references as output values, preferring to use pointer notation to make it explicit that an argument may be overwritten. + +A common structure for checking for messages is: + +```c++ +CANMessage msg; +while (can.read(msg)) { + // do something based on the received message +} +``` + +> `CANMessage` (of type `CAN_Message`) has the structure: +> +> ```c++ +> struct CAN_Message { +> unsigned int id; // 29 bit identifier +> unsigned char data[8]; // Data field +> unsigned char len; // Length of data field in bytes +> CANFormat format; // 0 - STANDARD, 1- EXTENDED IDENTIFIER +> CANType type; // 0 - DATA FRAME, 1 - REMOTE FRAME +> }; +> ``` +> +> For this lab, we'll only consider `id`, and `data`. + +The master node broadcasts its hue, in centi-degrees, as a 16-bit integer in the payload of a CAN message with id=0x43. You will have to unpack the byte-oriented data from the CAN message into a 16-bit hue (essentially using the opposite process in lab 2.3) and write it to the hue of the RGB LED. Use saturation = 65535 and value (brightness) = 32767. Don't forget that the hue should only be changed upon receiving a message with id=0x43 - there may be other network traffic on the CAN network. + +Once you're done, you can compare against the [reference solution](solutions/lab2.4.cpp). ## Extra for Experts Lab 2.5: Cooperative Multitasking Pulse a LED on for a second (or so) when a message is received, while doing all of Lab 2.2 and 2.3, using timer-based polling diff --git a/solutions/lab2.3.cpp b/solutions/lab2.3.cpp new file mode 100644 index 0000000..62a9ba7 --- /dev/null +++ b/solutions/lab2.3.cpp @@ -0,0 +1,32 @@ +#include "mbed.h" + +#include "ledutils.h" + +DigitalOut led1(P0_3); +DigitalOut led2(P0_9); + +DigitalIn btn(P0_4); + +RawSerial serial(P0_8, NC, 115200); + +CAN can(P0_28, P0_29); + +int main() { + // Initialize CAN controller at 1 Mbaud + can.frequency(1000000); + + // Button edge detection + bool lastButton = true; + + while (true) { + bool thisButton = btn; + if (thisButton != lastButton && btn == 0) { + uint16_t blinkLengthMs = 1000; + uint8_t data[2]; + data[0] = (blinkLengthMs >> 8) & 0xff; + data[1] = (blinkLengthMs >> 0) & 0xff; + can.write(CANMessage(0x41, (char*)data, 2)); + } + lastButton = thisButton; + } +} diff --git a/solutions/lab2.4.cpp b/solutions/lab2.4.cpp index 202425e..eaba0e4 100644 --- a/solutions/lab2.4.cpp +++ b/solutions/lab2.4.cpp @@ -22,7 +22,7 @@ int main() { while (can.read(msg)) { if (msg.id == 0x43) { uint16_t hue = (msg.data[0] << 8) | (msg.data[1] << 0); - rgbLed.hsv_uint16(hue, 65535, 65535); + rgbLed.hsv_uint16(hue, 65535, 32767); } } } From 7bf60bb4e08867e5cf475655ce58ba9a58f7f100 Mon Sep 17 00:00:00 2001 From: ducky64 Date: Mon, 6 Mar 2017 01:25:00 -0800 Subject: [PATCH 12/26] cooperative multitasking --- lab2.md | 47 ++++++++++++++++++++++++++++++++---- solutions/lab2.5.cpp | 51 ++++++++++++++++++++++++++++++++++++++++ solutions/lab2master.cpp | 2 +- 3 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 solutions/lab2.5.cpp diff --git a/lab2.md b/lab2.md index 0aa9174..c36b25e 100644 --- a/lab2.md +++ b/lab2.md @@ -52,7 +52,7 @@ TODO: ckt diagram In the example code skeletons, replace `RXD_PIN` and `TXD_PIN` with the appropriate pin name (like `P0_28`). Note that the example solutions use `P0_28` for RXD and `P0_29` for TXD. ## Lab 2.2: Sending CAN Messages -The master node is configured to pulse its white LED on for 0.25 seconds when receiving a CAN message with ID 0x42. +The master node is configured to pulse its LED on for 0.5 seconds when receiving a CAN message with ID 0x42. **Objective**: When the button is pressed on your BRAIN, have it transmit a message that triggers a blink on the remote master node. @@ -122,9 +122,9 @@ can.write(CANMessage(0x42)); Have your edge detection code execute the above on a button press, and you should be done. Feel free to compare against the [solution](solutions/lab2.2.cpp), too. ## Lab 2.3: CAN Messages with Data -When the master node receives a CAN message with ID 0x41, it will pulse its white LED with a length specified by the data field, in milliseconds. The blink length is the first 16-bit integer in the payload, in [big-endian (network order)](https://en.wikipedia.org/wiki/Endianness#Networking) format. +When the master node receives a CAN message with ID 0x41, it will pulse its LED with a length specified by the data field, in milliseconds. The blink length is the first 16-bit integer in the payload, in [big-endian (network order)](https://en.wikipedia.org/wiki/Endianness#Networking) format. -**Objective**: When the button is pressed on your BRAIN, have it transmit a message that triggers a blink on the remote master node at some interesting amount of time (not 250ms as the last lab). +**Objective**: When the button is pressed on your BRAIN, have it transmit a message that triggers a blink on the remote master node at some interesting amount of time (not 500ms as the last lab). `CANMessage` has another constructor: @@ -176,7 +176,7 @@ Start by declaring a RGB LED object. For convenience, a `RGBPwmLed` object has b RGBPwmOut rgbLed(P0_5, P0_6, P0_7); ``` -It has one API function, which sets the R, G, B PWM outputs using a input H, S, V. The hue is specified in centi-degrees [0, 36000), while the saturation and value are specified in 16-bit fixed point [0, 65535]. +It has one API function, which sets the R, G, B PWM outputs using an input H, S, V. The hue is specified in centi-degrees [0, 36000), while the saturation and value are specified in 16-bit fixed point [0, 65535]. ```c++ RGBPwmOut::hsv_uint16(uint16_t h_cdeg, uint16_t s, uint16_t v); @@ -217,7 +217,44 @@ The master node broadcasts its hue, in centi-degrees, as a 16-bit integer in the Once you're done, you can compare against the [reference solution](solutions/lab2.4.cpp). ## Extra for Experts Lab 2.5: Cooperative Multitasking -Pulse a LED on for a second (or so) when a message is received, while doing all of Lab 2.2 and 2.3, using timer-based polling +A microcontroller that can only do one thing is quite limiting, so let's put together the push-to-blink functionality (lab 2.2), the RGB LED hue update functionality (lab 2.4), and also the master LED pulse functionality. Note that the master code is set up such with the functionality in lab 2.2, where a button press causes it to send a CAN message with id=0x42. + +**Objective**: Upon receiving a CAN message with id=0x42, have your BRAIN pulse an LED on for 0.5s. While also updating the RGB LED hue from remote messages and sending a CAN message on a button press. + +### The Deceptively Simple Way + +The simplest way would be to add another message handler in the `can.read(msg)` loop: + +```c++ +CANMessage msg; +while (can.read(msg)) { + if (msg.id == 0x43) { + // hue code here + } else if (msg.id == 0x42) { + led2 = 1; + wait(0.5); + led2 = 0; + } +} +``` + +Try it, and it works, but only kind of. The problem is that the `wait` is blocking - it doesn't return, so the system can't do other things (like update the RGB LED hue from received CAN messages or detect button presses). This is noticeable: during the LED pulse time, the RGB LED hue will freeze, then jump to the newest received value. Obviously, this looks bad, and this _is_ bad. + +### The Robust Way +Most microcontrollers have built-in timer peripherals (essentially counters that tick in the background, independently of what the CPU is doing) and can be queried for the current count. mbed provides the `Timer` class as a hardware abstraction layer. A `Timer` has these operations: +- `Timer::start()`: starts the timer. Timers start off (not ticking). +- `Timer::reset()`: resets the passed time to 0. Does not change start / stopped state. +- `Timer::stop()`: self-explanatory. +- `Timer::read()`: reads the passed time, in seconds, as a float. +- `Timer::read_ms()`, `Timer::read_us()`: reads the passed time as an integer. + +Given that, we could restructure our code so that upon receiving the LED pulse message, the LED is turned on and a timer is started and reset. Then, regularly in the main loop, the timer is read and compared against the threshold time (500 ms here). Once it goes over the threshold, the LED is turned off. Since no blocking operations are performed, the RGB LED hue will continue updating while the LED is on. + +This style is called _cooperative multitasking_ because the different tasks must co-operate and yield control to other tasks regularly. For example, when we have a blocking wait in the deceptively simple method, the LED pulsing prevented the RGB LED hue from updating. This type of multitasking is simple, explicit (almost nothing going on "behind the scenes"), but also vulnerable to poor code like blocking operations. + +Implement the non-blocking pulsing LED, and verify that it works (RGB LED hue continues updating throughout the LED pulse such that you don't see visible hue discontinuities). Once you're done, you can check against the [reference solution](solutions/lab2.5.cpp). ## Extra for Experts Lab 2.6: Threaded Multitasking Pulse a LED on for a second (or so) when a message is received, while doing all of Lab 2.2 and 2.3, using mbed RTOS + +_With great power comes great responsibility_ diff --git a/solutions/lab2.5.cpp b/solutions/lab2.5.cpp new file mode 100644 index 0000000..1c40a4b --- /dev/null +++ b/solutions/lab2.5.cpp @@ -0,0 +1,51 @@ +#include "mbed.h" + +#include "ledutils.h" + +RGBPwmOut rgbLed(P0_5, P0_6, P0_7); + +DigitalOut led1(P0_3); +DigitalOut led2(P0_9); + +DigitalIn btn(P0_4); + +RawSerial serial(P0_8, NC, 115200); + +CAN can(P0_28, P0_29); + +int main() { + // LED Blink State + Timer ledTimer; // used to track blink length + ledTimer.start(); + + // Button edge detection + bool lastButton = true; + + // Initialize CAN controller at 1 Mbaud + can.frequency(1000000); + + while (true) { + // CAN receive handling + CANMessage msg; + while (can.read(msg)) { + if (msg.id == 0x43) { + uint16_t hue = (msg.data[0] << 8) | (msg.data[1] << 0); + rgbLed.hsv_uint16(hue, 65535, 32767); + } else if (msg.id == 0x42) { + led2 = 1; + ledTimer.reset(); + } + } + + // System actions + if (ledTimer.read_ms() >= 500) { + led2 = 0; + } + + bool thisButton = btn; + if (thisButton != lastButton && btn == 0) { + can.write(CANMessage(0x42)); + } + lastButton = thisButton; + } +} diff --git a/solutions/lab2master.cpp b/solutions/lab2master.cpp index 39b8b2d..be313bd 100644 --- a/solutions/lab2master.cpp +++ b/solutions/lab2master.cpp @@ -39,7 +39,7 @@ int main() { if (msg.id == 0x42) { led2 = 1; ledTimer.reset(); - blinkLengthMs = 250; + blinkLengthMs = 500; } else if (msg.id == 0x41) { led2 = 1; ledTimer.reset(); From 344e5da0d97fcadf5ab5b6902e0f24166b2a39d3 Mon Sep 17 00:00:00 2001 From: ducky64 Date: Mon, 6 Mar 2017 01:40:42 -0800 Subject: [PATCH 13/26] add rtos to build --- SConscript-env-lpc1549 | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/SConscript-env-lpc1549 b/SConscript-env-lpc1549 index ae77966..cae671f 100644 --- a/SConscript-env-lpc1549 +++ b/SConscript-env-lpc1549 @@ -6,6 +6,15 @@ env.ConfigureMbedTarget('LPC1549', File('mbed/targets/targets.json').srcnode()) # Build the mbed library as a static library mbed_paths = env.GetMbedSourceDirectories('mbed') env.Append(CPPPATH=[x.srcnode() for x in mbed_paths]) # this allows duplicate=0 + +rtos_paths = [ + Dir('mbed/rtos'), + Dir('mbed/rtos/rtx/TARGET_CORTEX_M'), + Dir('mbed/rtos/rtx/TARGET_CORTEX_M/TARGET_M3'), + Dir('mbed/rtos/rtx/TARGET_CORTEX_M/TARGET_M3/TOOLCHAIN_GCC'), +] +env.Append(CPPPATH=[x.srcnode() for x in rtos_paths]) # this allows duplicate=0 + env['MBED_LINKSCRIPT'] = env.GetMbedLinkscript(mbed_paths) env.Append(CPPDEFINES=[ 'MBED_CONF_PLATFORM_STDIO_BAUD_RATE=115200', @@ -13,7 +22,7 @@ env.Append(CPPDEFINES=[ 'MBED_CONF_PLATFORM_STDIO_FLUSH_AT_EXIT==1', 'MBED_CONF_PLATFORM_STDIO_CONVERT_NEWLINES=0' ]) - + env_mbed = env.Clone() env_mbed.Append(CCFLAGS='-w') # don't care about errors in dependencies mbed_sources = env.GetMbedSources(mbed_paths) @@ -21,6 +30,10 @@ mbed_sources.remove(File('mbed/targets/TARGET_NXP/TARGET_LPC15XX/device/system_L mbed_sources.append(File('common/lpc15xx_overrides/system_LPC15xx.c')) mbed_lib = env_mbed.StaticLibrary('mbed', mbed_sources) +rtos_sources = env.GetMbedSources(rtos_paths) +rtos_lib = env_mbed.StaticLibrary('rtos', rtos_sources) +env.Prepend(LIBS=rtos_lib) + env.Append(LINKFLAGS=[ '-Wl,--whole-archive', # used to compile mbed HAL, which uses funky weak symbols mbed_lib, From 5d6277c7b24cc272b3b014ef131c1056815cc455 Mon Sep 17 00:00:00 2001 From: ducky64 Date: Wed, 8 Mar 2017 00:05:02 -0800 Subject: [PATCH 14/26] RTOS lab 2.6 --- SConscript-env-lpc1549 | 5 ++- solutions/lab2.6.cpp | 84 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 solutions/lab2.6.cpp diff --git a/SConscript-env-lpc1549 b/SConscript-env-lpc1549 index cae671f..eb4110d 100644 --- a/SConscript-env-lpc1549 +++ b/SConscript-env-lpc1549 @@ -19,8 +19,9 @@ env['MBED_LINKSCRIPT'] = env.GetMbedLinkscript(mbed_paths) env.Append(CPPDEFINES=[ 'MBED_CONF_PLATFORM_STDIO_BAUD_RATE=115200', 'MBED_CONF_PLATFORM_DEFAULT_SERIAL_BAUD_RATE=115200', - 'MBED_CONF_PLATFORM_STDIO_FLUSH_AT_EXIT==1', - 'MBED_CONF_PLATFORM_STDIO_CONVERT_NEWLINES=0' + 'MBED_CONF_PLATFORM_STDIO_FLUSH_AT_EXIT=1', + 'MBED_CONF_PLATFORM_STDIO_CONVERT_NEWLINES=0', + 'OS_CLOCK=12000000', # for RTOS ]) env_mbed = env.Clone() diff --git a/solutions/lab2.6.cpp b/solutions/lab2.6.cpp new file mode 100644 index 0000000..7b9a8dc --- /dev/null +++ b/solutions/lab2.6.cpp @@ -0,0 +1,84 @@ +#include "mbed.h" +#include "rtos.h" + +#include "ledutils.h" + +RGBPwmOut rgbLed(P0_5, P0_6, P0_7); + +DigitalOut led1(P0_3); +DigitalOut led2(P0_9); + +DigitalIn btn(P0_4); + +RawSerial serial(P0_8, NC, 115200); + +CAN can(P0_28, P0_29); + +Mail canTransmitQueue; + +Thread ledThread; +Mail ledQueue; +void led_thread() { + while (true) { + osEvent evt = ledQueue.get(); + if (evt.status == osEventMail) { + uint16_t waitTime = *(uint16_t*)evt.value.p; + ledQueue.free((uint16_t*)evt.value.p); + led2 = 1; + Thread::wait(waitTime); + led2 = 0; + } + } +} + +Thread buttonThread; +void button_thread() { + bool lastButton = true; + while (true) { + bool thisButton = btn; + if (thisButton != lastButton && btn == 0) { + CANMessage* msg = canTransmitQueue.alloc(osWaitForever); + *msg = CANMessage(0x42); + canTransmitQueue.put(msg); + } + lastButton = thisButton; + + Thread::wait(5); + } +} + +int main() { + // Initialize CAN controller at 1 Mbaud + can.frequency(1000000); + + ledThread.start(led_thread); + buttonThread.start(button_thread); + + while (true) { + // CAN receive handling + CANMessage msg; + while (can.read(msg)) { + if (msg.id == 0x43) { + uint16_t hue = (msg.data[0] << 8) | (msg.data[1] << 0); + rgbLed.hsv_uint16(hue, 65535, 32767); + } else if (msg.id == 0x42) { + uint16_t* waitTime = ledQueue.alloc(osWaitForever); + *waitTime = 500; + ledQueue.put(waitTime); + } + } + + // CAN transmit handling + osEvent evt = canTransmitQueue.get(0); + while (evt.status == osEventMail) { + CANMessage msg = *(CANMessage*)evt.value.p; + canTransmitQueue.free((CANMessage*)evt.value.p); + led1 = !led1; + can.write(msg); + + evt = canTransmitQueue.get(0); + } + + Thread::yield(); + } +} From d3a096acfb5fda27d78ac59ba57b4e208c890891 Mon Sep 17 00:00:00 2001 From: ducky64 Date: Wed, 8 Mar 2017 01:22:39 -0800 Subject: [PATCH 15/26] RTOS lab --- lab2.md | 215 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 212 insertions(+), 3 deletions(-) diff --git a/lab2.md b/lab2.md index c36b25e..17bb129 100644 --- a/lab2.md +++ b/lab2.md @@ -222,7 +222,6 @@ A microcontroller that can only do one thing is quite limiting, so let's put tog **Objective**: Upon receiving a CAN message with id=0x42, have your BRAIN pulse an LED on for 0.5s. While also updating the RGB LED hue from remote messages and sending a CAN message on a button press. ### The Deceptively Simple Way - The simplest way would be to add another message handler in the `can.read(msg)` loop: ```c++ @@ -255,6 +254,216 @@ This style is called _cooperative multitasking_ because the different tasks must Implement the non-blocking pulsing LED, and verify that it works (RGB LED hue continues updating throughout the LED pulse such that you don't see visible hue discontinuities). Once you're done, you can check against the [reference solution](solutions/lab2.5.cpp). ## Extra for Experts Lab 2.6: Threaded Multitasking -Pulse a LED on for a second (or so) when a message is received, while doing all of Lab 2.2 and 2.3, using mbed RTOS +_While we currently don't use threading in our codebase, it's still a common programming model and is worthwhile to learn its features and pitfalls._ + +While cooperative multitasking accomplished our goals, it did so by spreading code around - for example, while we would interpret the LED blink as a logical unit, it actually ended up separated into two phases. In this section, we will explore a different approach to multitasking: threading and operating systems. + +**Objective**: Lab 2.5, but using the mbed RTOS. + +> A _thread_ is a sequence of instructions in a program. In the examples above, the entire program consisted of one thread, `main()`. However, with a _scheduler_ (a typical component in an _operating system_), it is possible to run multiple threads at once - typically, the scheduler interleaves the threads in time onto one processor. Across threads within a process, resources like memory are shared. +> +> There are many different models of communications between threads. The simplest, but most dangerous, is by manipulating shared memory. More structured communications channels are also available, like locks, semaphores, and queues. In this section, we will focus on queues as they are a robust while conceptually simple channel. +> +> A _real-time operating system_ is an operating system that attempts to meet real-time constraints, generally prioritizing latency over throughput. There are two types: _hard real-time_ systems are guaranteed to meet task deadlines, while _soft real-time_ systems provide no such guarantees. The mbed RTOS consists of a task scheduler but provides no task deadline guarantees (or even any static timing analysis capability), and is a soft real-time system. +> +> Multitasking in general is a hard problem, and this tutorial only covers the very basics. + +While the RTOS library has been included in the standard build, we haven't used it ... until now. Let's start by structuring our threads: +- the `main` thread will handle CAN communications, dispatching notifications to other threads based on received data +- a LED thread will blink the LED upon receiving a notification +- a button thread will notify the CAN thread to transmit a message + +Start by including the RTOS header: +```c++ +#include "rtos.h" +``` + +Then, declare two [`Thread`](https://developer.mbed.org/handbook/RTOS#thread) objects (note: `main` is implicitly its own thread): + +```c++ +Thread ledThread; +Thread buttonThread; +``` + +Each Thread will run a function, the skeletons of which are provided: + +```c++ +void led_thread() { + while (true) { + // TODO: pulse LED upon receiving notification + } +} + +void button_thread() { + bool lastButton = true; + while (true) { + bool thisButton = btn; + if (thisButton != lastButton && btn == 0) { + // TODO: enqueue a CAN message with id=0x42 for transmission upon button press + } + lastButton = thisButton; + + Thread::wait(5); + } +} +``` + +One important note is the use of `Thread::wait(uint32_t millisec)`, as opposed to bare `wait`. `Thread::wait` de-schedules the thread (allowing other threads to run as it is waiting), as opposed to spinning (which takes up compute resources doing nothing). As the button is sampled sufficiently slowly, there is no need to debounce. + +With the `Thread` objects and functions, we are now ready to write our `main` function: + +```c++ +int main() { + // Initialize CAN controller at 1 Mbaud + can.frequency(1000000); + + ledThread.start(led_thread); + buttonThread.start(button_thread); + + while (true) { + // CAN receive handling + CANMessage msg; + while (can.read(msg)) { + if (msg.id == 0x43) { + uint16_t hue = (msg.data[0] << 8) | (msg.data[1] << 0); + rgbLed.hsv_uint16(hue, 65535, 32767); + } else if (msg.id == 0x42) { + // TODO: notify the LED thread to pulse the LED + } + } + + // CAN transmit handling + // TODO: transmit CAN messages + + Thread::yield(); + } +} +``` + +You'll notice two new constructs here: +- `Thread::start(*fn)` starts a `Thread` object at a given function. From then on, the argument function and the calling thread will run in parallel. +- `Thread::yield()` "yields" the current thread, allowing other threads a chance to run. While the operating system will pre-empt (and switch to another thread) if one thread has been running for too long, proactively yielding when a thread has no more work (for example, waiting on input when polling) is good practice. + +Next, define the two communication channels. One will be a queue of pulse times for the LED, and another will be a queue of CAN messages to transmit: + +```c++ +Mail canTransmitQueue; +Mail ledQueue; +``` + +[`Mail`](https://developer.mbed.org/handbook/RTOS#mail) is a queue that stores elements (not to be confused with [`Queue`](https://developer.mbed.org/handbook/RTOS#queue), which can only store pointers. The first type parameter is the data type that is stored, and the second parameter is the number of elements in the queue. In the example above, `canTransmitQueue` consists of up to 16 elements of type `CANMessage`. + +`Mail`'s API is kind of funny: +- To enqueue an element, you first need to allocate storage space using `Mail::alloc()`, which returns a pointer to an element that you need to initialize. Then, you can actually enqueue the element using `Mail::put(elem)` For example, to enqueue a wait time of 500ms into `ledQueue`: + + ```c++ + uint16_t* waitTime = ledQueue.alloc(osWaitForever); + *waitTime = 500; + ledQueue.put(waitTime); + ``` + + Note: `Mail::alloc()` takes an optional parameter of the maximum time to wait for a free element buffer. In the above, we essentially do a blocking allocation, waiting indefinitely until one is available. If a finite time is passed in, `alloc` can fail and return `NULL`. +- To dequeue an element, use `osEvent evt = Mail::get()`, which returns an object of type `osEvent`. The object's `status` field indicates the event's meaning, and we are interested in the case when `evt.status == osEventMail`. For a mail event, we can get a pointer to the element using `evt.value.p` followed by a typecast. Remember to `Mail::free()` the pointer when done so the element memory can be re-used. An example for polling and reading `ledQueue` is: + + ```c++ + osEvent evt = ledQueue.get(); + if (evt.status == osEventMail) { + uint16_t waitTime = *(uint16_t*)evt.value.p; + ledQueue.free((uint16_t*)evt.value.p); + led2 = 1; + Thread::wait(waitTime); + led2 = 0; + } + ``` + + You can see that all the LED code is cleanly centralized into one location. Also note that `Mail::get()` takes an optional maximum wait time too, defaulting to forever (essentially blocking until a message is available). + +Putting the two above examples together, our full code now looks like: + +```c++ +#include "mbed.h" +#include "rtos.h" + +#include "ledutils.h" + +RGBPwmOut rgbLed(P0_5, P0_6, P0_7); + +DigitalOut led1(P0_3); +DigitalOut led2(P0_9); + +DigitalIn btn(P0_4); + +RawSerial serial(P0_8, NC, 115200); + +CAN can(P0_28, P0_29); + +Mail canTransmitQueue; + +Thread ledThread; +Mail ledQueue; +void led_thread() { + while (true) { + osEvent evt = ledQueue.get(); + if (evt.status == osEventMail) { + uint16_t waitTime = *(uint16_t*)evt.value.p; + ledQueue.free((uint16_t*)evt.value.p); + led2 = 1; + Thread::wait(waitTime); + led2 = 0; + } + } +} + +Thread buttonThread; +void button_thread() { + bool lastButton = true; + while (true) { + bool thisButton = btn; + if (thisButton != lastButton && btn == 0) { + // TODO: enqueue a CAN message with id=0x42 for transmission upon button press + } + lastButton = thisButton; + + Thread::wait(5); + } +} + +int main() { + // Initialize CAN controller at 1 Mbaud + can.frequency(1000000); + + ledThread.start(led_thread); + buttonThread.start(button_thread); + + while (true) { + // CAN receive handling + CANMessage msg; + while (can.read(msg)) { + if (msg.id == 0x43) { + uint16_t hue = (msg.data[0] << 8) | (msg.data[1] << 0); + rgbLed.hsv_uint16(hue, 65535, 32767); + } else if (msg.id == 0x42) { + uint16_t* waitTime = ledQueue.alloc(osWaitForever); + *waitTime = 500; + ledQueue.put(waitTime); + } + } + + // CAN transmit handling + // TODO: transmit CAN messages + + Thread::yield(); + } +} +``` + +Test that it works, that you can simultaneously blink the LED while still updating the RGB LED. Afterwards, fill out the two TODOs in the same style as the LED enqueue / dequeue operations. + +Once you've given it a shot, check out the [reference solution](solutions/lab2.6.cpp). + +### Discussion +_With great power comes great responsibility._ + +Threading provides a different approach than cooperative multitasking, helping to centralize related sequences of instructions but now making the actual instruction execution sequence less predictable. While we used safe inter-thread communications mechanisms above (with `Mail`), the threads actually can read and write each other's memory, and not being careful can lead to subtle, non-deterministic bugs like [race conditions](https://en.wikipedia.org/wiki/Race_condition). -_With great power comes great responsibility_ +There are also more implementation limitations of the mbed RTOS, like maximum number of threads and predefined stack sizes. One issue hindering debuggability is that when the RTOS fails, it can't give a helpful error message - so another potential pitfall. From 0964ea72ac7edda467a950fa70549548cdb93805 Mon Sep 17 00:00:00 2001 From: ducky64 Date: Thu, 9 Mar 2017 01:42:06 -0800 Subject: [PATCH 16/26] rewrite threading discussion --- lab2.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/lab2.md b/lab2.md index 17bb129..7ac7333 100644 --- a/lab2.md +++ b/lab2.md @@ -464,6 +464,18 @@ Once you've given it a shot, check out the [reference solution](solutions/lab2.6 ### Discussion _With great power comes great responsibility._ -Threading provides a different approach than cooperative multitasking, helping to centralize related sequences of instructions but now making the actual instruction execution sequence less predictable. While we used safe inter-thread communications mechanisms above (with `Mail`), the threads actually can read and write each other's memory, and not being careful can lead to subtle, non-deterministic bugs like [race conditions](https://en.wikipedia.org/wiki/Race_condition). +Now that we've implemented the same functionality in both cooperative multitasking and threaded forms, let's compare. -There are also more implementation limitations of the mbed RTOS, like maximum number of threads and predefined stack sizes. One issue hindering debuggability is that when the RTOS fails, it can't give a helpful error message - so another potential pitfall. +What did we gain with threading? +- Related sequences of instructions appear as a single unit in code, even if their execution is separated by other threads. This is apparent in the LED pulsing code. +- The appearance of multitasking without needing to handle the details manually, as with cooperative multitasking. +- Communication between tasks are made explicit with the use of `Mail` queues. + +What pitfalls did we avoid? +- The use of `Mail` queues provides a principled way to communicate between threads. Threading actually exposes a shared memory model, where threads can read and write each other's memory, and not being careful and methodical can lead to subtle, non-deterministic bugs like [race conditions](https://en.wikipedia.org/wiki/Race_condition). +- The mbed RTOS has implementation limitations, like maximum number of threads and predefined maximum stack size. While this trivial example stayed within the limits, the RTOS isn't always able to produce a helpful error message on failure, making debugging painful. + +What did we lose with threading? +- The actual execution behavior of the RTOS isn't predictable, especially with regards to very fine (milliseconds) timing. While the thread scheduler gives the illusion of multiple threads executing simultaneously, the actual thread swapping occurs at human timescales (every few milliseconds) rather than machine timescales. + +Overall, threading provides a different approach than cooperative multitasking and can be a powerful tool in certain situations, but only if you're aware of the pitfalls and shortcomings. If you're interested in using threading for your project, do talk to us so we can discuss whether it's appropriate and how to mitigate the potential issues. From 4f88e30c88ade1abd41a7d9ee68bc60f9a0d1ebf Mon Sep 17 00:00:00 2001 From: Devan Lai Date: Sun, 12 Mar 2017 03:22:43 -0700 Subject: [PATCH 17/26] Add CAN->USB debugging using the SLCAN protocol This commit adds a lab2slcan variant of the lab2master node firmware that reports CAN messages over USB using the CAN-USB/SLCAN[1] protocol. The lab2slcan firmware and supporting files can be found under the util/ directory. Some CAN-USB commands have not been fully implemented. For compatibility, several commands are accepted, but have no effect. In particular, the lab2slcan firmware will not allow the host PC to change the CAN mode or bitrate, nor will it apply any hardware message filters or timestamping. The lab2slcan firmware has been tested for compatibility with USBtinViewer[2] and the UAVCAN GUI Tool[3]. 1: http://www.can232.com/docs/canusb_manual.pdf 2: http://www.fischl.de/usbtin/#software 3: http://uavcan.org/GUI_Tool/Overview/ --- SConscript | 19 +- SConscript-env-lpc1549 | 29 +++ util/NonBlockingUSBSerial.cpp | 16 ++ util/NonBlockingUSBSerial.h | 32 +++ util/lab2slcan.cpp | 127 ++++++++++ util/slcan.cpp | 461 ++++++++++++++++++++++++++++++++++ util/slcan.h | 50 ++++ util/usb_slcan.cpp | 164 ++++++++++++ util/usb_slcan.h | 56 +++++ 9 files changed, 949 insertions(+), 5 deletions(-) create mode 100644 util/NonBlockingUSBSerial.cpp create mode 100644 util/NonBlockingUSBSerial.h create mode 100644 util/lab2slcan.cpp create mode 100644 util/slcan.cpp create mode 100644 util/slcan.h create mode 100644 util/usb_slcan.cpp create mode 100644 util/usb_slcan.h diff --git a/SConscript b/SConscript index 635f4c6..67541c7 100644 --- a/SConscript +++ b/SConscript @@ -1,7 +1,8 @@ -import os.path +import os, os.path Import('env_lpc1549') +# Environment for building projects for labs env = env_lpc1549.Clone() env.Append(CCFLAGS=['-Wall', '-Werror']) @@ -10,10 +11,10 @@ env.Append(CCFLAGS=['-Wall', '-Werror']) # be shared with solution compilation main_all = Glob('src/*.cpp') main_srcs = [elt for elt in main_all if elt.name == 'main.cpp'] -mainlibs_srcs = [elt for elt in main_all if elt not in main_srcs] +support_srcs = [elt for elt in main_all if elt not in main_srcs] -mainlibs = env.StaticLibrary('mainlibs', mainlibs_srcs) -env.Append(LIBS=mainlibs) +supportlib = env.StaticLibrary('mainlibs', support_srcs) +env.Append(LIBS=supportlib) env.Default(env.CalSolFW('brain', srcs=main_srcs, @@ -28,7 +29,15 @@ for solution in solutions: srcs=[solution], includes=['src'] )) - + +# Build the master node with SLCAN debugging +env.Default( + env.CalSolFW('util/lab2slcan', + srcs=Glob('util/*.cpp'), + includes=['util','src'] + ) +) + env.Alias('prog', env.Command('openocd', 'brain.elf', 'openocd -f interface/cmsis-dap.cfg -f lpc1549_openocd.cfg -c init -c "reset halt" -c "flash erase_sector 0 0 last" -c "flash write_image $SOURCE" -c "reset run" -c "exit"' diff --git a/SConscript-env-lpc1549 b/SConscript-env-lpc1549 index eb4110d..3dbd877 100644 --- a/SConscript-env-lpc1549 +++ b/SConscript-env-lpc1549 @@ -1,4 +1,6 @@ # Modifies the environment to build mbed firmware for the LPC1549 using GCC-ARM +import os, os.path + Import('env') env.ConfigureMbedTarget('LPC1549', File('mbed/targets/targets.json').srcnode()) @@ -24,6 +26,30 @@ env.Append(CPPDEFINES=[ 'OS_CLOCK=12000000', # for RTOS ]) +# Build the USBDevice library as a dependency +usb_class_dirs = [ + 'mbed/features/unsupported/USBDevice/USBAudio', + 'mbed/features/unsupported/USBDevice/USBHID', + 'mbed/features/unsupported/USBDevice/USBMIDI', + 'mbed/features/unsupported/USBDevice/USBMSD', + 'mbed/features/unsupported/USBDevice/USBSerial', +] + +usb_device_dirs = [ + 'mbed/features/unsupported/USBDevice/USBDevice', +] + +usb_srcs = [ + 'mbed/features/unsupported/USBDevice/USBDevice/USBDevice.cpp', + 'mbed/features/unsupported/USBDevice/USBDevice/USBHAL_LPC11U.cpp', +] + +for path in usb_class_dirs: + usb_srcs.extend(Glob(os.path.join(path,'*.cpp'))) + +usb_dirs = usb_class_dirs + usb_device_dirs +env.Append(CPPPATH=usb_dirs) + env_mbed = env.Clone() env_mbed.Append(CCFLAGS='-w') # don't care about errors in dependencies mbed_sources = env.GetMbedSources(mbed_paths) @@ -35,6 +61,9 @@ rtos_sources = env.GetMbedSources(rtos_paths) rtos_lib = env_mbed.StaticLibrary('rtos', rtos_sources) env.Prepend(LIBS=rtos_lib) +usb_lib = env_mbed.StaticLibrary('usbdevice', usb_srcs) +env.Append(LIBS=usb_lib) + env.Append(LINKFLAGS=[ '-Wl,--whole-archive', # used to compile mbed HAL, which uses funky weak symbols mbed_lib, diff --git a/util/NonBlockingUSBSerial.cpp b/util/NonBlockingUSBSerial.cpp new file mode 100644 index 0000000..f263790 --- /dev/null +++ b/util/NonBlockingUSBSerial.cpp @@ -0,0 +1,16 @@ +#include "NonBlockingUSBSerial.h" + +NonBlockingUSBSerial::NonBlockingUSBSerial(uint16_t vendor_id, uint16_t product_id, uint16_t product_release, bool connect_blocking) + : USBSerial(vendor_id, product_id, product_release, connect_blocking) { + +} + +bool NonBlockingUSBSerial::writeBlockNB(uint8_t* buf, uint16_t size) { + if (size <= MAX_PACKET_SIZE_EPBULK && configured()) { + /* Return true if the data was enqueued successfully + Enqueued data may be lost if the host disconnects, but data + will almost certainly be lost in that case anyways */ + return (endpointWrite(EPBULK_IN, buf, size) == EP_PENDING); + } + return false; +} diff --git a/util/NonBlockingUSBSerial.h b/util/NonBlockingUSBSerial.h new file mode 100644 index 0000000..eb3260f --- /dev/null +++ b/util/NonBlockingUSBSerial.h @@ -0,0 +1,32 @@ +#ifndef NONBLOCKINGUSBSERIAL_H_ +#define NONBLOCKINGUSBSERIAL_H_ +#include +#include + +class NonBlockingUSBSerial : public USBSerial { +public: + /** + * Constructor + * + * @param vendor_id Your vendor_id (default: 0x1f00) + * @param product_id Your product_id (default: 0x2012) + * @param product_release Your product_release (default: 0x0001) + * @param connect_blocking define if the connection must be blocked if USB not plugged in (default: false) + * + */ + NonBlockingUSBSerial(uint16_t vendor_id = 0x1f00, uint16_t product_id = 0x2012, uint16_t product_release = 0x0001, bool connect_blocking = false); + + /** + * Write a block of data without waiting. + * + * For more efficiency, a block of size 64 (maximum size of a bulk endpoint) has to be written. + * + * @param buf pointer on data which will be written + * @param size size of the buffer. The maximum size of a block is limited by the size of the endpoint (64 bytes) + * + * @returns true if successful + */ + bool writeBlockNB(uint8_t* buf, uint16_t size); +}; + +#endif diff --git a/util/lab2slcan.cpp b/util/lab2slcan.cpp new file mode 100644 index 0000000..8e85235 --- /dev/null +++ b/util/lab2slcan.cpp @@ -0,0 +1,127 @@ +#include "mbed.h" + +#include "ledutils.h" +#include "slcan.h" +#include + +RGBPwmOut rgbLed(P0_5, P0_6, P0_7); + +DigitalOut led1(P0_3); +DigitalOut led2(P0_9); + +DigitalIn btn(P0_4); + +NonBlockingUSBSerial serial(0x1209, 0x0001, 0x0001, false); + +CAN can(P0_28, P0_29); +USBSLCANSlave slcan(serial); + +const uint16_t DEBOUNCE_TIME_MS = 50; +const uint16_t RGB_LED_UPDATE_MS = 20; // a respectable 50 Hz + +// CAN message transmit helper to echo messages over SLCAN +static bool transmitAndEchoCANMessage(const CANMessage& msg) { + if (can.write(msg) == 1) { + // Echo anything that at least made it to the CAN controller + // This does not imply that the message was acknowledged. + if (serial.configured()) { + slcan.putCANMessage(msg); + } + return true; + } + return false; +} + +// Helper to allow the host to send CAN messages +static bool transmitCANMessage(const CANMessage& msg) { + return (can.write(msg) == 1); +} + +int main() { + // LED Blink State + int32_t blinkLengthMs = 0; + Timer ledTimer; // used to track blink length + ledTimer.start(); + + bool lastButton = true; + Timer buttonDebounceTimer; + + uint16_t hue = 0; + Timer rgbLedTimer; + rgbLedTimer.start(); + + // Initialize CAN controller at 1 Mbaud + can.frequency(1000000); + + // Allow the SLCAN interface to transmit messages + slcan.setTransmitHandler(&transmitCANMessage); + + // Silently ignore commands to change the mode/bitrate + // for compatibility with USBtinViewer + slcan.setIgnoreConfigCommands(true); + + while (true) { + // CAN receive handling + CANMessage msg; + while (can.read(msg)) { + slcan.putCANMessage(msg); + if (msg.id == 0x42) { + led2 = 1; + ledTimer.reset(); + blinkLengthMs = 500; + } else if (msg.id == 0x41) { + led2 = 1; + ledTimer.reset(); + blinkLengthMs = (msg.data[0] << 8) | msg.data[1]; + } + } + + // System actions + bool thisButton = btn; + if (thisButton != lastButton) { + buttonDebounceTimer.start(); + if (buttonDebounceTimer.read_ms() >= DEBOUNCE_TIME_MS) { + // Actual edge occurs here + lastButton = thisButton; + + // Do edge actions + if (thisButton == false) { + transmitAndEchoCANMessage(CANMessage(0x42)); + } + } + } + if (thisButton == lastButton) { + // This also gets triggered on a fresh edge. + buttonDebounceTimer.stop(); + buttonDebounceTimer.reset(); + } + + if (ledTimer.read_ms() >= blinkLengthMs) { + led2 = 0; + } + + if (rgbLedTimer.read_ms() >= RGB_LED_UPDATE_MS) { + rgbLedTimer.reset(); + + hue += RGB_LED_UPDATE_MS * 10; + hue = hue % 36000; // 360 degree * 100 + + rgbLed.hsv_uint16(hue, 65535, 32767); + + // Update CAN with new hue. + uint8_t data[2]; + data[0] = (hue >> 8) & 0xff; + data[1] = (hue >> 0) & 0xff; + transmitAndEchoCANMessage(CANMessage(0x43, (char*)data, 3)); + } + + // Process SLCAN commands and report CAN messages + if (serial.configured()) { + slcan.update(); + } else { + // Try reconnecting to the USB host + slcan.reset(); + serial.connect(false); + } + } +} diff --git a/util/slcan.cpp b/util/slcan.cpp new file mode 100644 index 0000000..30b8724 --- /dev/null +++ b/util/slcan.cpp @@ -0,0 +1,461 @@ +#include "slcan.h" + +// Helper methods for parsing commands +static bool parse_hex_digits(const char* input, uint8_t num_digits, uint32_t* value_out) { + bool success = true; + uint32_t value = 0; + + uint8_t i; + for (i=0; i < num_digits; i++) { + uint32_t nibble = 0; + if (input[i] >= '0' && input[i] <= '9') { + nibble = 0x0 + (input[i] - '0'); + } else if (input[i] >= 'a' && input[i] <= 'f') { + nibble = 0xA + (input[i] - 'a'); + } else if (input[i] >= 'A' && input[i] <= 'F') { + nibble = 0xA + (input[i] - 'A'); + } else { + success = false; + break; + } + uint8_t offset = 4*(num_digits-i-1); + value |= (nibble << offset); + } + + if (success) { + *value_out = value; + } + + return success; +} + +static bool parse_hex_values(const char* input, uint8_t num_values, uint8_t* values_out) { + uint8_t i; + for (i=0; i < num_values; i++) { + uint32_t value; + if (parse_hex_digits(input, 2, &value)) { + values_out[i] = (uint8_t)value; + } else { + return false; + } + input += 2; + } + + return true; +} + +static bool parse_dec_digit(const char* input, uint8_t* value_out) { + if (input[0] >= '0' && input[0] <= '9') { + *value_out = 0 + (input[0] - '0'); + return true; + } else { + return false; + } +} + +static inline char format_nibble(uint8_t x) { + uint8_t nibble = x & 0x0F; + return (nibble < 10) ? ('0' + nibble) : ('A' + (nibble - 10)); +} + +static inline char format_digit(uint8_t d) { + return '0' + d; +} + +SLCANBase::SLCANBase() { + +} + +SLCANBase::~SLCANBase() { + +} + +uint8_t SLCANBase::getFirmwareVersion() { + // firmware version in BCD + return 0x10; +} + +uint8_t SLCANBase::getHardwareVersion() { + // hardware version in BCD + return 0x10; +} + +const char* SLCANBase::getSerialString() { + // 4 character serial number + return "C254"; +} + +bool SLCANBase::update() { + bool active = false; + if (processCommands()) { + active = true; + } + if (processCANMessages()) { + active = true; + } + + if (flush()) { + active = true; + } + + return active; +} + +size_t SLCANBase::formattedCANMessageLength(const CANMessage& msg) { + size_t len; + if (msg.format == CANStandard) { + len = 1 + 3 + 1 + (2 * msg.len) + 1; + } else { + len = 1 + 8 + 1 + (2 * msg.len) + 1; + } + + return len; +} + +size_t SLCANBase::formatCANMessage(const CANMessage& msg, char* buf, size_t max_len) { + size_t len = formattedCANMessageLength(msg); + if (len > max_len) { + return 0; + } + + if (msg.format == CANStandard) { + *buf++ = (msg.type == CANData) ? 't' : 'r'; + *buf++ = format_nibble((uint8_t)(msg.id >> 8)); + *buf++ = format_nibble((uint8_t)(msg.id >> 4)); + *buf++ = format_nibble((uint8_t)(msg.id >> 0)); + *buf++ = format_digit(msg.len); + } else { + *buf++ = (msg.type == CANData) ? 'T' : 'R'; + *buf++ = format_nibble((uint8_t)(msg.id >> 28)); + *buf++ = format_nibble((uint8_t)(msg.id >> 24)); + *buf++ = format_nibble((uint8_t)(msg.id >> 20)); + *buf++ = format_nibble((uint8_t)(msg.id >> 16)); + *buf++ = format_nibble((uint8_t)(msg.id >> 12)); + *buf++ = format_nibble((uint8_t)(msg.id >> 8)); + *buf++ = format_nibble((uint8_t)(msg.id >> 4)); + *buf++ = format_nibble((uint8_t)(msg.id >> 0)); + *buf++ = format_digit(msg.len); + } + + for (unsigned char i=0; i < msg.len; i++) { + *buf++ = format_nibble((uint8_t)(msg.data[i] >> 4)); + *buf++ = format_nibble((uint8_t)(msg.data[i] >> 0)); + } + + *buf++ = '\r'; + + return len; +} + +size_t SLCANBase::commandResponseLength(const char* command) { + switch (command[0]) { + case 'N': + case 'V': { + return 6; + } + case 'v': + case 'F': { + return 4; + } + case 'T': + case 't': + case 'R': + case 'r': { + return 2; + } + default: { + return 1; + } + } +} + +bool SLCANBase::execCommand(const char* command, char* response) { + bool success = false; + switch (command[0]) { + // Configuration commands + case 'S': + case 's': + case 'O': + case 'L': + case 'l': + case 'C': + case 'Z': + case 'M': + case 'm': { + success = execConfigCommand(command); + break; + } + // Transmission commands + case 't': + case 'T': + case 'r': + case 'R': { + success = execTransmitCommand(command, response); + break; + } + // Diagnostic commands + case 'V': + case 'v': + case 'N': + case 'W': + case 'F': + success = execDiagnosticCommand(command, response); + break; + default: { + success = false; + break; + } + } + + return success; +} + +bool SLCANBase::execConfigCommand(const char* command) { + bool success = false; + size_t len = strlen(command); + + // Validate command length + if (command[0] == 'M' || command[0] == 'm') { + if (len != 9) { + return false; + } + } else if (command[0] == 's') { + if (!((len == 5) || (len == 7))) { + return false; + } + } else if (command[0] == 'S' || command[0] == 'Z') { + if (len != 2) { + return false; + } + } else if (len != 1) { + return false; + } + + switch (command[0]) { + case 'S': { + bool known = true; + int baudrate; + switch (command[1]) { + case '0': baudrate = 10000; break; + case '1': baudrate = 20000; break; + case '2': baudrate = 50000; break; + case '3': baudrate = 100000; break; + case '4': baudrate = 125000; break; + case '5': baudrate = 250000; break; + case '6': baudrate = 500000; break; + case '7': baudrate = 800000; break; + case '8': baudrate = 1000000; break; + default: known = false; break; + } + + if (known) { + success = setBaudrate(baudrate); + } + + break; + } + case 'O': { + success = setMode(CAN::Normal); + break; + } + case 'L': { + success = setMode(CAN::Silent); + break; + } + case 'l': { + success = setMode(CAN::SilentTest); + break; + } + case 'C': { + success = setMode(CAN::Reset); + break; + } + case 's': { + // TODO: implement direct BTR control + success = true; + break; + } + case 'M': + case 'm': { + // TODO: implement filtering + success = true; + break; + } + case 'Z': { + // TODO: implement timestamping + success = true; + break; + } + default: { + success = false; + break; + } + } + + return success; +} + +bool SLCANBase::execTransmitCommand(const char* command, char* response) { + bool success = false; + + size_t len = strlen(command); + + bool validMessage = false; + CANMessage msg; + + if (command[0] == 't' || command[0] == 'T') { + msg.type = CANData; + msg.format = (command[0] == 't') ? CANStandard : CANExtended; + size_t idLen = msg.format == CANStandard ? 3 : 8; + if ((len >= idLen + 2) && + parse_hex_digits(&command[1], idLen, (uint32_t*)&msg.id) && + parse_dec_digit(&command[idLen + 1], &msg.len)) { + if ((len == idLen + 2 + 2*msg.len) && + (msg.len <= 8) && + parse_hex_values(&command[idLen + 2], msg.len, msg.data)) { + validMessage = true; + } + } + } else if (command[0] == 'r' || command[0] == 'R') { + msg.type = CANRemote; + msg.format = (command[0] == 'r') ? CANStandard : CANExtended; + size_t idLen = msg.format == CANStandard ? 3 : 8; + if ((len == idLen + 2) && + parse_hex_digits(&command[1], idLen, (uint32_t*)(&msg.id)) && + parse_dec_digit(&command[idLen + 1], &msg.len)) { + if (msg.len <= 8) { + validMessage = true; + } + } + } + + if (validMessage) { + if (command[0] == 'T' || command[0] == 'R') { + response[0] = 'Z'; + } else if (command[0] == 't' || command[0] == 'r') { + response[0] = 'z'; + } + success = transmitMessage(msg); + } + + return success; +} + +bool SLCANBase::execDiagnosticCommand(const char* command, char* response) { + bool success = false; + size_t len = strlen(command); + + // Validate command length + if (command[0] == 'W') { + if (len != 5) { + return false; + } + } else if (len != 1) { + return false; + } + + if (!response) { + return false; + } + + switch (command[0]) { + case 'V': { + success = true; + uint8_t hwVersion = getHardwareVersion(); + uint8_t fwVersion = getFirmwareVersion(); + + response[0] = 'V'; + response[1] = format_nibble(hwVersion >> 4); + response[2] = format_nibble(hwVersion >> 0); + response[3] = format_nibble(fwVersion >> 4); + response[4] = format_nibble(fwVersion >> 0); + break; + } + case 'v': { + success = true; + uint8_t fwVersion = getFirmwareVersion(); + response[0] = 'v'; + response[1] = format_nibble(fwVersion >> 4); + response[2] = format_nibble(fwVersion >> 0); + break; + } + case 'N': { + success = true; + const char* serial = getSerialString(); + response[0] = 'N'; + int i; + for (i=0; i < 4; i++) { + char c = serial[i]; + if (c == '\0') { + break; + } else { + response[i+1] = c; + } + } + + // Pad the serial with spaces if it's short + for (; i < 4; i++) { + response[i+1] = ' '; + } + break; + } + case 'F': { + success = true; + uint8_t status = 0x00; + response[0] = 'F'; + response[1] = format_nibble(status >> 4); + response[2] = format_nibble(status >> 0); + } + case 'W': { + // Just swallow the MCP2515 register write command + success = true; + break; + } + default: { + success = false; + break; + } + } + + return success; +} + +/* Read and buffer one command if possible */ +bool SLCANBase::readCommand() { + bool active = false; + while (!commandQueued && inputReadable()) { + char c = (char)readInputByte(); + if (c == '\r') { + if (commandOverflow) { + // Replace with a dummy invalid command so we return an error + inputCommandBuffer[0] = '!'; + inputCommandBuffer[1] = '\0'; + inputCommandLen = 0; + commandOverflow = false; + active = true; + } else { + // Null-terminate the buffered command + inputCommandBuffer[inputCommandLen] = '\0'; + //stream.puts(inputCommandBuffer); + inputCommandLen = 0; + active = true; + } + if (inputCommandBuffer[0] != '\0') { + commandQueued = true; + } + } else if (c == '\n' && inputCommandLen == 0) { + // Ignore line feeds immediately after a carriage return + } else if (commandOverflow) { + // Swallow the rest of the command when overflow occurs + } else { + // Append to the end of the command + inputCommandBuffer[inputCommandLen++] = c; + + if (inputCommandLen >= sizeof(inputCommandBuffer)) { + commandOverflow = true; + } + } + } + + return active; +} diff --git a/util/slcan.h b/util/slcan.h new file mode 100644 index 0000000..4675ab7 --- /dev/null +++ b/util/slcan.h @@ -0,0 +1,50 @@ +#ifndef SLCAN_H_ +#define SLCAN_H_ + +#include + +class SLCANBase { +public: + bool update(); + virtual ~SLCANBase(); + +protected: + SLCANBase(); + + // To be implemented by subclasses + virtual bool setBaudrate(int baudrate) = 0; + virtual bool setMode(CAN::Mode mode) = 0; + virtual bool transmitMessage(const CANMessage& msg) = 0; + + virtual bool processCommands() = 0; + virtual bool processCANMessages() = 0; + virtual bool flush() = 0; + virtual bool inputReadable() const = 0; + virtual int readInputByte() = 0; + + virtual uint8_t getFirmwareVersion(); + virtual uint8_t getHardwareVersion(); + virtual const char* getSerialString(); + + // Shared amongst subclasses + static size_t formatCANMessage(const CANMessage& msg, char* buf, size_t max_len); + static size_t formattedCANMessageLength(const CANMessage& msg); + static size_t commandResponseLength(const char* command); + bool execCommand(const char* command, char* response); + bool readCommand(); + + bool commandQueued; + char inputCommandBuffer[32]; + +private: + bool execConfigCommand(const char* command); + bool execTransmitCommand(const char* command, char* response); + bool execDiagnosticCommand(const char *command, char* response); + + bool commandOverflow; + size_t inputCommandLen; +}; + +#include "usb_slcan.h" + +#endif diff --git a/util/usb_slcan.cpp b/util/usb_slcan.cpp new file mode 100644 index 0000000..843fb78 --- /dev/null +++ b/util/usb_slcan.cpp @@ -0,0 +1,164 @@ +#include "slcan.h" + +USBSLCANBase::USBSLCANBase(NonBlockingUSBSerial& stream) + : stream(stream), + messageQueued(false), + outputPacketLen(0) { + +} + +/* Check if any bytes are available at the input */ +bool USBSLCANBase::inputReadable() const { + return stream.readable(); +} + +/* Read a single byte from the input */ +int USBSLCANBase::readInputByte() { + return stream.getc(); +} + +/* Parse and execute a single SLCAN command and enqueue the response */ +bool USBSLCANBase::processCommands() { + // Buffer an entire command + bool active = readCommand(); + + // Process the current command if there's space to send the response + if (commandQueued) { + size_t responseLength = commandResponseLength(inputCommandBuffer); + if ((outputPacketLen + responseLength) <= sizeof(outputPacketBuffer)) { + char* packetTail = &outputPacketBuffer[outputPacketLen]; + if (execCommand(inputCommandBuffer, packetTail)) { + // Success + packetTail[responseLength-1] = '\r'; + outputPacketLen += responseLength; + } else { + // Failure + outputPacketBuffer[outputPacketLen++] = '\a'; + } + commandQueued = false; + active = true; + } + } + + return active; +} + +/* Read and enqueue as many received CAN messages as will fit */ +bool USBSLCANBase::processCANMessages() { + bool active = false; + + size_t bytesAvailable = sizeof(outputPacketBuffer) - outputPacketLen; + char* packetTail = &outputPacketBuffer[outputPacketLen]; + + if (messageQueued) { + size_t bytesConsumed = formatCANMessage(queuedMessage, packetTail, bytesAvailable); + if (bytesConsumed > 0) { + active = true; + messageQueued = false; + bytesAvailable -= bytesConsumed; + packetTail += bytesConsumed; + outputPacketLen += bytesConsumed; + } + } + + if (!messageQueued) { + while (getNextCANMessage(queuedMessage)) { + size_t bytesConsumed = formatCANMessage(queuedMessage, packetTail, bytesAvailable); + if (bytesConsumed > 0) { + active = true; + bytesAvailable -= bytesConsumed; + packetTail += bytesConsumed; + outputPacketLen += bytesConsumed; + } else { + messageQueued = true; + break; + } + } + } + + return active; +} + +/* Attempt to transmit the output queue */ +bool USBSLCANBase::flush() { + bool active = false; + if (outputPacketLen > 0) { + bool sent = stream.writeBlockNB((uint8_t*)(outputPacketBuffer), + (uint16_t)(outputPacketLen)); + if (sent) { + active = true; + outputPacketLen = 0; + } + } + return active; +} + +/* Reset internal buffers because the host disconnected */ +void USBSLCANBase::reset() { + outputPacketLen = 0; + messageQueued = false; +} + +/* an SLCAN implementation that only accesses the CAN peripheral through callbacks */ +USBSLCANSlave::USBSLCANSlave(NonBlockingUSBSerial& stream) + : USBSLCANBase(stream), + ignoreConfigCommands(false) { +} + +/* Reset internal buffers because the host disconnected */ +void USBSLCANSlave::reset() { + USBSLCANBase::reset(); + messageBuffer.reset(); +} + +/* Configure SLCAN to silently discard mode/baudrate commands */ +void USBSLCANSlave::setIgnoreConfigCommands(bool ignore) { + ignoreConfigCommands = ignore; +} + +/* Register the handler to change the CAN bitrate on request */ +void USBSLCANSlave::setBaudrateHandler(Callback callback) { + cbSetBaudrate.attach(callback); +} + +/* Register the handler to change the CAN mode on request */ +void USBSLCANSlave::setModeHandler(Callback callback) { + cbSetMode.attach(callback); +} + +/* Register the handler to send a CAN message on request */ +void USBSLCANSlave::setTransmitHandler(Callback callback) { + cbTransmitMessage.attach(callback); +} + +bool USBSLCANSlave::setBaudrate(int baudrate) { + if (ignoreConfigCommands) { + return true; + } else { + return cbSetBaudrate.call(baudrate); + } +} + +bool USBSLCANSlave::setMode(CAN::Mode mode) { + if (ignoreConfigCommands) { + return true; + } else { + return cbSetMode.call(mode); + } +} + +bool USBSLCANSlave::transmitMessage(const CANMessage& msg) { + return cbTransmitMessage.call(msg); +} + +bool USBSLCANSlave::putCANMessage(const CANMessage& msg) { + if (!messageBuffer.full()) { + messageBuffer.push(msg); + return true; + } + return false; +} + +bool USBSLCANSlave::getNextCANMessage(CANMessage& msg) { + return messageBuffer.pop(msg); +} diff --git a/util/usb_slcan.h b/util/usb_slcan.h new file mode 100644 index 0000000..a462428 --- /dev/null +++ b/util/usb_slcan.h @@ -0,0 +1,56 @@ +#ifndef USB_SLCAN_H_ +#define USB_SLCAN_H_ + +#include "slcan.h" +#include +#include +#include + +class USBSLCANBase : public SLCANBase { +public: + virtual void reset(); + +protected: + USBSLCANBase(NonBlockingUSBSerial& stream); + + virtual bool inputReadable() const; + virtual int readInputByte(); + virtual bool processCommands(); + virtual bool processCANMessages(); + virtual bool flush(); + + // To be implemented by subclasses + virtual bool getNextCANMessage(CANMessage& msg) = 0; +private: + NonBlockingUSBSerial& stream; + CANMessage queuedMessage; + bool messageQueued; + + char outputPacketBuffer[64]; + size_t outputPacketLen; +}; + +class USBSLCANSlave : public USBSLCANBase { +public: + USBSLCANSlave(NonBlockingUSBSerial& stream); + + virtual void reset(); + bool putCANMessage(const CANMessage& msg); + void setIgnoreConfigCommands(bool ignore); + void setBaudrateHandler(Callback callback); + void setModeHandler(Callback callback); + void setTransmitHandler(Callback callback); +protected: + virtual bool setBaudrate(int baudrate); + virtual bool setMode(CAN::Mode mode); + virtual bool transmitMessage(const CANMessage& msg); + virtual bool getNextCANMessage(CANMessage& msg); +private: + CircularBuffer messageBuffer; + Callback cbSetBaudrate; + Callback cbSetMode; + Callback cbTransmitMessage; + bool ignoreConfigCommands; +}; + +#endif From 4e9f38ae3e1d227bad94ff35fbbd8050370ccf81 Mon Sep 17 00:00:00 2001 From: ducky64 Date: Sun, 12 Mar 2017 13:05:35 -0700 Subject: [PATCH 18/26] fix some code, use non-RTR frames --- solutions/lab2.2.cpp | 2 +- solutions/lab2.5.cpp | 2 +- solutions/lab2.6.cpp | 3 +-- solutions/lab2master.cpp | 4 ++-- util/lab2slcan.cpp | 4 ++-- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/solutions/lab2.2.cpp b/solutions/lab2.2.cpp index 0632be2..a791589 100644 --- a/solutions/lab2.2.cpp +++ b/solutions/lab2.2.cpp @@ -21,7 +21,7 @@ int main() { while (true) { bool thisButton = btn; if (thisButton != lastButton && btn == 0) { - can.write(CANMessage(0x42)); + can.write(CANMessage(0x42, NULL, 0)); } lastButton = thisButton; } diff --git a/solutions/lab2.5.cpp b/solutions/lab2.5.cpp index 1c40a4b..dd2b79a 100644 --- a/solutions/lab2.5.cpp +++ b/solutions/lab2.5.cpp @@ -44,7 +44,7 @@ int main() { bool thisButton = btn; if (thisButton != lastButton && btn == 0) { - can.write(CANMessage(0x42)); + can.write(CANMessage(0x42, NULL, 0)); } lastButton = thisButton; } diff --git a/solutions/lab2.6.cpp b/solutions/lab2.6.cpp index 7b9a8dc..c26b6b4 100644 --- a/solutions/lab2.6.cpp +++ b/solutions/lab2.6.cpp @@ -38,7 +38,7 @@ void button_thread() { bool thisButton = btn; if (thisButton != lastButton && btn == 0) { CANMessage* msg = canTransmitQueue.alloc(osWaitForever); - *msg = CANMessage(0x42); + *msg = CANMessage(0x42, NULL, 0); canTransmitQueue.put(msg); } lastButton = thisButton; @@ -73,7 +73,6 @@ int main() { while (evt.status == osEventMail) { CANMessage msg = *(CANMessage*)evt.value.p; canTransmitQueue.free((CANMessage*)evt.value.p); - led1 = !led1; can.write(msg); evt = canTransmitQueue.get(0); diff --git a/solutions/lab2master.cpp b/solutions/lab2master.cpp index be313bd..550bac1 100644 --- a/solutions/lab2master.cpp +++ b/solutions/lab2master.cpp @@ -57,7 +57,7 @@ int main() { // Do edge actions if (thisButton == false) { - can.write(CANMessage(0x42)); + can.write(CANMessage(0x42, NULL, 0)); } } } @@ -83,7 +83,7 @@ int main() { uint8_t data[2]; data[0] = (hue >> 8) & 0xff; data[1] = (hue >> 0) & 0xff; - can.write(CANMessage(0x43, (char*)data, 3)); + can.write(CANMessage(0x43, (char*)data, 2)); } } } diff --git a/util/lab2slcan.cpp b/util/lab2slcan.cpp index 8e85235..607051e 100644 --- a/util/lab2slcan.cpp +++ b/util/lab2slcan.cpp @@ -86,7 +86,7 @@ int main() { // Do edge actions if (thisButton == false) { - transmitAndEchoCANMessage(CANMessage(0x42)); + transmitAndEchoCANMessage(CANMessage(0x42, NULL, 0)); } } } @@ -112,7 +112,7 @@ int main() { uint8_t data[2]; data[0] = (hue >> 8) & 0xff; data[1] = (hue >> 0) & 0xff; - transmitAndEchoCANMessage(CANMessage(0x43, (char*)data, 3)); + transmitAndEchoCANMessage(CANMessage(0x43, (char*)data, 2)); } // Process SLCAN commands and report CAN messages From 8548495c7afb363781610442ac35f93c5b4d796d Mon Sep 17 00:00:00 2001 From: ducky64 Date: Sun, 12 Mar 2017 13:56:01 -0700 Subject: [PATCH 19/26] Lab 2 updates --- docs/usbtin-interface.png | Bin 0 -> 23069 bytes docs/usbtin-monitor.png | Bin 0 -> 16132 bytes docs/usbtin-trace.png | Bin 0 -> 47734 bytes lab1.md | 40 ++++++++++++++++++----------------- lab2.md | 43 ++++++++++++++++++++++++++------------ 5 files changed, 51 insertions(+), 32 deletions(-) create mode 100644 docs/usbtin-interface.png create mode 100644 docs/usbtin-monitor.png create mode 100644 docs/usbtin-trace.png diff --git a/docs/usbtin-interface.png b/docs/usbtin-interface.png new file mode 100644 index 0000000000000000000000000000000000000000..b91abed42a221bb0409723c76b58398bbd100aa0 GIT binary patch literal 23069 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 literal 0 HcmV?d00001 diff --git a/docs/usbtin-monitor.png b/docs/usbtin-monitor.png new file mode 100644 index 0000000000000000000000000000000000000000..c5439e3990fc2cccaea7a01e7926aba150c9da80 GIT binary patch literal 16132 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