Electric VehiclesJune 23, 2018

Discover the EVTV ESP32 CANDue Board, featuring a powerful 240MHz dual-core ESP32 chip with WiFi and Bluetooth BLE. Experience 6x performance and dual CAN ports for advanced connectivity.

This is the next generation of EVTV embedded microcontroller. It features a 240MHz dual-core ESP32 chip with built-in Wifi and Bluetooth BLE for 6x the performance of the EVTV CANDue Microcontroller plus wireless capability to interact with the Internet (you can run a web server on it if you can believe it) and communicate with smartphone devices such as iPhone and iPAD. It features TWO CAN ports including one for CAN Flexible Data Rate - CAN FD, the new standard for block firmware updates over CAN.

We include a high-quality plastic enclosure allowing ample room for shields with a gland nut to route wiring.

Click here for more info about the EVTV ESP32 CANDue Board

This device is rated at 600DMIPS - basically the equivalent of 600 VAX 11/780 minicomputers of 1979

Support for the ESPRESSIF ESP32 chip has been added to the Arduino IDE allowing us to easily program the EVTV ESP32 CANDue board using the familiar Arduino IDE and the wealth of libraries available for it.

Our own Collin Kidder has written a new, unified library all inclusive of code needed to make it operate. Implements a CAN driver for the built-in CAN hardware on an ESP32. Also implements a driver for the MCP2517FD SPI connected CAN module. The builtin CAN is called CAN0, the MCP2517FD is called CAN1. This library is specifically meant to be used with the EVTV ESP32-Due board. Aside from CAN FD, this library can be used to support other ESP32 boards with CAN transceivers.

Collin has also developed a new ESP32RET program to make the EVTV ESP32 CANDue the ultimate CAN reverse engineering tool. With this software, you can connect to your vehicle's CAN bus and communicate with Collins SAVVYCan CAN analysis suite WIRELESSLY. SavvyCAN is available for Linux, Windows, and MAC OSX.

Many of our future controller projects will be based on this microcontroller board to take advantage of the higher speeds, dual CAN ports, faster FD data rates, and wireless features. Most importantly, it offers us the ability to interface with smartphone devices and tablets for graphical interfaces.

Here we share this core tool of our arsenal with you for your own projects.

ESP32 CANDue. New board features 2 CAN ports and ESP32 controller

Okay, our test setup is the Activa EVTV ESP32 CAN-DUE board side-by-side. They're each connected to USB ports to power them and let me get a serial output. And the CAN-1 port, that's the port using the new MCP2517FD chip, those two are connected.

And we're going to do some CAN testing of that. And so let's take a look at the program I've done. This program basically goes and gets an NTP time mark.

It logs on my local Wi-Fi router and gets an NTP time mark. And then it uses that to print the time to the screen and updates that time internally without going back. It just fetches the time once and then updates the time and prints it to the screen.

But it also takes that time and transmits it to the other board over CAN. And of course the other board's doing the same thing and running the same programs. It doesn't really need the time.

We're simply wanting to test our CAN connectivity over CAN-1. Our second port, CAN0, is the native CAN port in the ESP32 chip. And CAN-1 is the one we added with the MCP2517FD chip.

And so this little program goes and gets the time and then sends it to the other board over CAN. And it prints the time out to the screen. And here I have the two up on the screen using the screen monitor, or we know IDE, to connect to one.

And Cool Terms to connect to the other one. And the other one is actually our previous prototype. And the Arduino IDE serial monitor is connected to our new prototype, which has the notch out from under the antenna.

And let me show you first the difference there. I've added a function to the program where I can enter a W and it will go get the time mark again. But to do that, it has to log on to find my wireless router and connect to it.

And when it does that, it gets a RSSI reading of the received signal sensitivity. And that's kind of a measurement of the connection that we get. And so on the new one, I'm going to enter W. And we have it connected to Riverhouse with a signal strength of minus 72 dBm.

Now, lower values of this negative dBm are a better connectivity. So 72 is pretty good, and then 80 or 90 or so on would be a weaker signal. 70, 60, 50 would be a stronger one.

So I can simply repeat this several times. There's 72 dBm. There's 70 dBm.

So we'll get a little different reading every time. There's 72, 73. That's a function of wireless interference, for one thing, and of course environmentals.

But that's a real signal strength. If I go to the second one and enter W, I get the same thing. But here we're 81 dBm.

And here is, again, minus 81 dBm. That's a much weaker received signal strength. And the wireless router, obviously, is transmitting at the same power the whole time.

But my reception is going to be better on the one with the notch on it. There's 81, 81. And up here, same time, 72, 74, 74.

73. So about 8 or 9 dBm different. In some conditions, my initial testing of this, I had as much as 18 dBm difference in the received signal strength.

And I routinely get 8 or 9 dBm. So that's the reception on the radio. But I said that we do CAN transmissions.

And I can enter a capital D here. And all of a sudden, I'm just printing a lot of stuff up on the screen. Let me stop that.

And you can see I have a sent message at 4 hours 38 minutes and 44.566 seconds. So this is our milliseconds. And then received, I received a 707 message.

They're both transmitting the same message at 44.581. So if I subtract those, it's like 66 to 81 is 15 milliseconds later. More to the point, I'm transmitting at 0.586. So here's 581601 on the receive. So I'm sending every 20 milliseconds.

And I'm also receiving every 20 milliseconds on that screen. And I can duplicate that down here. Oops, that's a little bug.

I want debug. And there we go. I can actually change the data rates.

I've said every 20 milliseconds. Let's go to interval 10, I10. And that should change my transmitted data rate every 10 milliseconds.

And so here we see I'm at 606, 616, 626. But my received is 601 and 621. So I'm still receiving every 20 milliseconds and sending every 10.

So let's go down here and set this one to I10. And now I should be. Here I sent it 907.

I sent it 927. I sent it 937. And I'm receiving at 899.

And here I have a received time of 882. Received message 859. So I have these kind of botched up serial lines.

And if I go up here, maybe we can see the same thing. It depends. Here I received at 240.

Here I received, but I didn't really finish printing it. And then I received at 256. That's odd.

That's like 16 milliseconds later. But I entered 10. Here it received, and it's interrupted.

And sent at 286 and 296. And received at 289 and 312. Well, that's 20-something milliseconds.

So how can that be if down here I'm sending every 10 milliseconds? And let's take a look at that. Let's disconnect sent at 707 and sent at 717. So something's a problem here, wouldn't you say? We're just not cutting the mustard.

What the problem is, is printing all this data to this screen. Remember I said that the CAN works on interrupt callbacks. As soon as we receive a message, we go process it.

And what we mostly do in processing it, in this case, is derive the time from the message. And we print this string to the screen. But it's kind of a lengthy screen.

I'm saying sent, message ID, and then the message ID. This is a first byte, second byte, third byte, fourth byte, fifth byte, sixth byte, seventh byte, eighth byte. Then I'm calculating the time that I sent it.

And this is kind of the number of items I've sent in the last four hours or whatever. And so there's a lot going on here in this print string. And Arduino has always had a little bit of an issue with their serial port printing algorithms.

What can we do about that? Well, let's turn debug off here and here. And then we're not printing that. But I can't see how often we're transmitting and receiving CAN messages now, can I? So we need another debug algorithm.

And I actually have coded one. And that uses little d. And we can enter that on each of them. And what this does, let's show it down here.

This shows my transmit time that I'm transmitting with an asterisk. I precede it with an asterisk. And all I do is print the time when I send the message.

And when I receive a message, I print a period. And I print the time. And that's all I print.

I don't even put print a carriage return. Here's the transmitted and received and transmitted and received. So we can see here on the transmit, I'm sending at 277 milliseconds.

Here's at 287 milliseconds. Here's at 297 milliseconds. Here's at 307, 317.

So I'm getting a very regular 10 millisecond transmission. My reception is at 283, 293, 303, 313, 323. And so you can see that we're actually sending and receiving at very precise times and very effectively at 10 milliseconds between each message.

Now a word on that. Most CAN messages are every 100 milliseconds. Some of them are every second.

A few will be every 50 milliseconds. And very rarely a control message for a drive inverter, for example, that has to be constantly updated very fast might use 20 milliseconds between CAN frames. Well, here we are at 10.

And we're in perfect operation on two devices. And so that's working pretty good. Let's go to five.

And we're getting into some unusual territory here now. And let me disconnect that. And we are sending at 260 and sending at 265 and sending at 270 and sending at 275 and sending at 280 and sending at 285.

While we're receiving at 263, 273, 283. So this is working great with this one, sending every five milliseconds and receiving on 10 milliseconds. And I can go up here and change this to I5.

And lo and behold, down here, I'm now receiving at 218, 223, 228, 233, 238. So that's every five milliseconds. And 215, 220, 225, 230.

So we're sending and receiving and printing to the screen, by the way. There's still some overhead in our printing. It's just much less than that big long stream that we were printing before.

But still, most of our time here is not really being spent with CAN. It's being spent with printing out the serial port. Let's take this to an extreme.

And I'm going to say I2. And we'll go up here and say I2. And let's go down here and disconnect and see what we get.

2140, 2142, 2144, 2146, 2148, 2150. I'm sending it. 2142, 2144, 146, 148, 150.

We're not missing any here, guys. We are both sending and receiving in perfect order at 2 milliseconds a frame. Sending the entire frame that you saw printed out before.

Calculating it, stuffing it in the frame, and transmitting it. And still, most of our time is being eaten up by this serial printing to the screen. But we're doing it at a 2 millisecond interval on both boards.

So they're both screaming at each other on a 2 millisecond increment. Just blasting CAN frames every 2 milliseconds. And we're not missing a trick here.

Even though we have to stop and print to the screen the asterisk or period and the time. And so you can see that this dual-channel 240 MHz device is doing a bang-up job of sending and receiving CAN messages over our new channels. And we've repeated this, of course, testing through the native channel.

And they both work just as well. So that's our test of two things. One of our wireless sensitivity and one of our CAN traffic capability.

And this is just way, acres, and miles past what you'll ever need to capture high rates of CAN message traffic without dropped frames. And that's the point here. That's the important point.

We can do these 500 kilobit per second CAN buses. And we're doing frames perfectly and without error between two devices at a 2 millisecond increment. And so that's kind of an astounding result, I think.

We're very enthusiastic about this. And, of course, as you saw, an 8 or 10 dB improvement in our wireless connectivity. Our ability to both send and receive wireless transmissions has been seriously upgraded simply by notching out the circuit board under the antenna.

And making the antenna a little more free in space to receive and transmit. And essentially resonate to a 2.4 GHz wireless frequency, which is what is used for both Wi-Fi and Bluetooth World Energy. Let's take a look at something that's very cool.

And it's about our CAN library. Well, let's take a look at the program first so you can see our programming technique to primarily deal with CAN. And that's the heart of our contribution with this board to the ESP32 thing, which is catching on rather well anyway, quite without us.

But we're bringing to the table some CAN expertise and some additional function to do a couple of CAN ports and at these rates of speed to make that all work. And so our primary function is CAN. All the BLE and wireless Wi-Fi and pre-RTOS functions you can use this board to do all the ESP32 things.

It can be a webpage server. I mean, it can be a web server. It has tons of memory.

It can monitor sensors and put that up on the World Wide Web very easily. We can obviously do Internet connectivity things like fetch a time, which is pretty trivial, but it works quite well. And all the other things that people are doing with ESP32.

And, of course, one of the main areas of interest for us, but we don't have to develop very much, is the Bluetooth BLE or Bluetooth Low Energy. That gives us the main means of interfacing with smartphone and tablet apps, both Apple iPhone, iPad, and Android phones and tablets. The main way you do that today with applications is over Bluetooth BLE.

The Internet of Things is based on Bluetooth BLE, and this lets you build devices that are fully compliant with that. Now, there's many very inexpensive and quite capable ESP32 circuits out there, very inexpensive. But this one is, in the first place, a good way to learn because it's big and lets you turn on LEDs and stuff very easily using the familiar Arduino Dewey form factor.

But this is specifically designed and offered as a CAM interface to the ESP32, and so that's what we're doing here. Let's talk a little bit about the underlying code. One of the attractions with the ESP32 chip is that it's now been incorporated into a board type for the Arduino IDE.

Arduino was started by Massimo Banzi a number of years ago as an educational device for microcontrollers. There's 5 million people using Arduino today. The IDE, or Integrated Design Environment, we really like, not because it's the best IDE out there.

It's free, it's easy to install, it works on all three platforms, Linux, Windows, and Mac OS X. It has some built-in functions for dealing with hardware microcontrollers. We like that environment because it's easy to communicate with our customers and other users about how to do things. It enables them to use the same hardware we sell for other purposes we had not envisioned quite easily.

The Arduino program, they call it Sketch, it actually uses the GNU GCC or C++ compiler underneath anyway, so it is C++. It's not like C++, it is C++. The ESP32 has a software development kit interface, but it can be quite icky and complex to deal with simple hardware functions like turning on a pin, output pin, or reading an input pin.

We prefer the Arduino interface, and someone has done an Arduino interface for the ESP32 SDK. This is an Arduino Sketch, or program, the only difference between that and a normal C++ program. Instead of having main, you have a setup function that runs when you first bring up the equipment and power on.

And then it has a loop function that it simply performs over and over again. And that's kind of like main and C or C++. First thing I do in most of my programs is declare a template because of an omission in the Arduino implementation here.

And that is the streaming operator to left carets. And I like to be able to send things to the standard output, or in this case the USB serial port, using that technique. And so I declare a template, and it makes that the operator, and I can send variables, I can send strings, anything out the USB port, and it simply makes it easier.

Then the Arduino print statements. One of the advantages of the Arduino environment is the 5 million guides, and it's hardware related. You tend to use Arduino microcontrollers to control some kind of hardware.

A humidity sensor, or a moisture sensor, or a light sensor, or something like that. Those tend to have fairly detailed requirements to read and write to them and program them. And people tend to use the object oriented nature of C++.

To make an object that has a simple interface and handles all that detail work within the object, and they call those libraries. Using an include statement, we can simply compile that library into our code. And so it becomes available to call in our functions using the syntax provided for that library.

You often have to read me for the library to learn how to do that, but it sure is reusable code done by other people to do icky tasks that we don't want to fool with. And reinvent the wheel in every program. And so that's a big part of Arduino.

And why we like Arduino is the body of libraries out there. And they make doing programming much easier. We're going to include one called ESPTASKWDT, or watchdog timer.

And that's a watchdog timer library. Watchdog timer is something that simply runs a clock. And if it isn't updated by the program within a specified time frame, it reboots the computer.

And so if you write a program that kind of gets lost in your code because you didn't do it right, or some power glitch or something causes it to derail, this hardware clock will basically trip a reboot of the system, and it'll come back up and resume operation. So we like that function. We have an SPI library.

It's the Serial Portfolio Interface. And that's a kind of serial communication, but it's one of the fastest ones. It's not one of the most noise resistant, but for short distances, it can be up to 2 megabits on a serial connection.

We use it in our CAN library, actually. And this is the CAN library, ESP32 CAN. That's what Colin Kidder wrote, and we kind of collaborated on that.

He did the heavy lifting, and I did the arm waving, and couldn't you do the set and the other. To develop a version of his previous very successful Dewey CAN library, but extended for the ESP32 specifically, and also supporting our new implementation using the MCP2517FD chip for CAN flexible data rate. And that's our CAN1.

Time is a library. Somebody did to deal with the network time protocol. We go out on the internet and synchronize our clock to the time servers at the National Institute of Science and Technology in Boulder, Colorado.

And that will get us synced up with time in the world very nicely. And Wi-Fi is a library to let us use the Wi-Fi radio in the ESP32 very well. And so when we include those, the compiled version of that code is added into our compiled output program.

And so our routines can access the data structures and the calls in those libraries, and we don't have to rewrite all that. Structured variables. These are not simple variables.

They are variables used in C++, but you can make your own type of variables to be fairly specific. And we do routinely. The main one here is CANFrame.

That actually is a variable out of our ESP32 CAN library. That's a structured variable for handling CAN data. We do an out frame and a frame.

And so that's the name of our variables, and these are their types. And then we have a structured variable for the time library. These are almost really objects, but they're structures of type TM from time.

And we're going to call them TimeInfo and TimeInfo2. And so that gives us two variables of the proper structure to work with the methods in Time.h. This is object instantiation. It's kind of like structured variables, but it's actually using a class and instantiating an object of that class.

And that's what we need to do with ESPTaskWatchDogTimer. Actually, it's what we need to do with the ESP32 ClockTimer, a hardware timer. And so we're going to use that to send CAN data.

And so HWTimerT is the class. We're going to instantiate an object of that class called My707Timer. And we're going to initialize that at null, and we'll fill that in later.

Function declarations. Boy, I don't know what to tell you. Because using some of the ESP32 CAN structures in the methods or in these functions, for some reason I have to declare them up here so I can use them later in a program, and I define them later in a program.

I've got to tell you, I'm not sure why I have to do that. We'll have to ask Colin. Global variables.

These are just variables I use in the program. You want to minimize that. But they can be used to communicate between functions and methods for state values.

And so we'll use them as little semaphores and things to change the state of our program. The setup function. This is all front matter here.

Our libraries, templates, objects, functions, global variables. Now we get into kind of the meat of the melon. And the Arduino convention is that you have a function called setup.

You can call it something else. It has to be called setup. And that's what the Arduino interface IDE program will compile as main, really.

That's what it comes up in when the power is applied to the multi-controller. Here's the start point of the program. And it runs once.

And then after it has run, the machine goes to loop. And again, you can't change this name. It has to be called loop.

And it's a specific function that will be executed. And when it's completed, it will go back and execute it again. And that's why they call it loop.

It's just going to run this loop forever. You can call functions out of this loop, but they then will return to this loop. And so this just runs forever.

So our setup function, the first thing I do is serial begin 115200. That sets our USB serial port for output. And that's typically text that we display on the screen.

And we can use that to input text to intercommands to the program. And when that serial begin is completed, we say serial up and running. And there is our streaming operator right there.

I just say serial and send it a string, up and running, and it will print it. Our next item in setup is get time mark. And that is a function that we're going to use to go out on the internet, get the NIST time mark.

And we have to access our Wi-Fi router to do that and so forth. So let's go down and see if we can find that. I kind of put it at the bottom of the program.

Here it is, get time mark. And the first thing it does is call a function start Wi-Fi with the SID and password. And that is the name of my Wi-Fi router, my logon password.

And that is right here, this function. So it calls this function first. The first official act we have to do is logon to the router.

And we saw this in our program operations. In that I could press W and it would go do it again. And what that does is say I'm out to the port I'm searching for and the name of the router it's searching for.

And then we do Wi-Fi begin out of our Wi-Fi library. And send it the SSID and password. And then we say while it's not connected.

And I'll delay for 200 milliseconds and print a period. And I'll keep doing that on the way out of this loop is if I get connected or if the attempts to connect. If I go through this loop 30 times and never break out.

I'm going to print the screen fail to connect. And I'm going to perform an ESP32 restart. That's simply going to reboot the machine.

And so we'll make 30 passes here 200 milliseconds each waiting for our Wi-Fi radio to connect to the router. And if it doesn't do it in that period of time we're going to reboot the machine and try again. And so we'll do that continuously until we do.

If we get WL connected being true from our Wi-Fi function. Wi-Fi.status. Then we break out and say connected to the Wi-Fi local IP variable. Out of the Wi-Fi object we'll list this IP number.

Signal strength and I can retrieve the Wi-Fi.rssi. Receive signal sensitivity. A value of merit from that. And so I print that out of the screen and that's how we were able to program operation.

To detect how sensitive our receiver was between the two versions of the board. So we're back in time mark. We did the start Wi-Fi function right here and we connected.

If we didn't connect we just keep rebooting. Once we do connect we return here. And I say config time 00 and pool.ntp.org. And this is out of our time library.

It has a method called config time and you can use different time servers. We're just using what's called the pool out of using the network time protocol. They've set up a kind of an internet structure to that you can access.

And it will kind of route you to the least busy closest time server to where you are. It's not really geographic it's in the geography of the internet protocol. And so the least number of hops and the least busy server and it does that kind of intelligently.

So we like to use this pool and that is the preferred method of accessing time servers. Unless you have a specific reason to access a specific time server. Like your own local time server or something.

This is a preferred method and lets them manage stuff. If one of the time servers is taken down to rewired or for maintenance or something. Requests are simply routed to other time servers.

Set environment or set env is again a function of our time library. And this is all kind of a bunch of gibberish but it sets us for central standard time. Which is universal coordinated time minus six hours here in Missouri.

And it will change and update on the proper date in April or the proper date in November. So I don't have to worry about that. And you can look in the time library to kind of decode the rest of this.

I'm not going to go into it. It puts us in the right time zone and using the right time that we want to use. If not get local time and time info.

Fail to get time mark. Get local time is we've configured it for this time server and this environment. And now we're going to actually go get local time.

And we're going to store it in that structured variable we created before time info. The ampersand is the address of. And so we're giving it the location here where we want to get local time.

And if we can't get it, this will return a one. And we'll print serial. Failed to get time mark.

We tried and it didn't come. If we do get it, we're going to have a global variable. Time of day valid.

And we're going to set to true. This is kind of a state variable that we say, yeah, you have a valid time in the machine. And we're going to print on the screen, retrieving time mark via NTP, network time protocol.

But in fact, we've already retrieved it. And so we don't have to be going to retrieve it. We've already got it, but you won't notice that on the screen.

And then we go to our Wi-Fi object and we turn the Wi-Fi off completely. Once we have our time mark, we don't want to spend any power on Wi-Fi at all. And we simply shut it off and print Wi-Fi disconnected to the screen.

And so that's one of the first things we do in setup is get our time mark. And that's basically fire up a radio, access our home router, go to a network time protocol server, get the time mark and shut off Wi-Fi. Remember, we instantiated a hardware timer object or variable called my 707 timer.

And we're going to set that to timer begin 380 true. And that is timer three. I've got this set up as timer two in the comments.

It's actually three. We're going to use one millisecond increments, which is our clock divided by 80 winds up being one millisecond. And we're going to count up true is for up.

And so we have a timer. We're going to set it to zero. We're going to use the hardware timer three.

We're going to tick it in one millisecond increments. We're going to count up. Then we're going to attach an interrupt to that my 707 timer.

And we're going to call that trigger 707. And true means we're going to do it on the rising edge of that timer. We'll trigger an interrupt and go perform the function at trigger 707.

The alarm right again, my 707 timer 20,000 is this is microseconds. And so by setting this to 20,000, we're setting it to 20 milliseconds. And then true means we're going to auto reload it.

And so our hardware timer, it is timer three, one millisecond increments. One microsecond increments actually. And at 20,000 microseconds, we're going to trigger an interrupt and go perform the function trigger 707.

Okay. And so that's to send a can frame every 20,000 or every 20 milliseconds. Our next setup is initialize can.

And we're going to initialize our can variables or can objects. That's really the main portion of our program description today. We can't cover all the things you can do with the ESP32 chip.

This is about using ESP32 can to do can communications. So let me hold that explanation in abeyance for a second. And so we can complete our function here of setup.

And here we set timer alarm enable my 707 timer. We configured it all up here. And we're not going to set it to go off.

We can't send can frames every 20 milliseconds. We haven't even initialized can yet. So we initialize our can ports.

And then we enable the timer alarm hardware timer to send frames. And then we're going to do a couple of things with, remember we had ESPTASKWDT watchdog timer. Here's our initialization.

We're going to set it for five seconds. And at five seconds we're going to panic. Truly it's like reboot the machine.

And here's the same object from the library. And we're going to add this task for the watchdog. And what that means is if it didn't get fed every five seconds, it's going to reboot the machine.

It's just the watchdog. Now here is a, this is a whole function actually. And remember that we said if we got a interrupt from our hardware timer, and we're going to have an interrupt every 20 milliseconds, that we're going to perform this function.

Here's the address of trigger 707. Well here is the function trigger 707. Got it kind of jammed together on one line.

Here let me fix you. Does that look more familiar? And all this function does is do 707 trend is a global variable. And we're going to set it to true.

Now why such a little bitty function? Well this goes off every 20 milliseconds. Our program is going to be in a loop doing things. When we interrupt it, we don't really want to go send the can message right away.

We want to do this in kind of orderly fashion, and not like interrupt right in the middle of a print statement or something. And so all we're going to do when that timer goes off is set this to true. So it's not really going to go send the frame every 20 milliseconds.

It's going to set a flag to send the train, the thing. So every 20 milliseconds this hardware timer goes off. It executes this function and returns.

What it returns to is loop. So we're sitting near a loop and doing our thing, and our hardware timer goes off. At 20 milliseconds, we don't want it to do very much.

We want to get back in our loop. And so we'll just have it send that flag. Now down in the loop, at the end of the loop when we've done everything we need to do, I'm going to say if do 707 send, call the function send 707 to end frame.

So you see how this works. The global variable is set to true. And at my leisure, when I've done what I need to do, I'm going to then go send it.

But I don't really have much going on in the loop either. I feed my watchdog. I do an ESP task WT reset.

That means my five second watchdog timer is reset to zero. And as long as I keep doing that, it'll never go off. And then I call a time function, which is get local time out of my time library, and put it in a structured variable we set up earlier called time info.

And that just updates the time. I'm going to print that, so I need to go update that time. Now understand it doesn't go back out to the network time protocol on the server.

It simply updates all the configured variables in the local time object. Now this is kind of tricky. If loop count not equal time info dot tmsec, then we're going to print the time.

And the first thing we do is set loop count, this variable, again a global variable, to the time info dot tmsec. So as long as this does equal the current second, this is just going to skip. We're not going to do any of this.

And at the point where our get local time results in a second that has updated, at that point we'll print it. Once we reset the second, and so on the next pass through loop, it's going to be the same second. And it will be for a number of passes.

We can do hundreds, maybe thousands of passes through loop. In a second. So we're only going to update or print that out on the screen when basically on the second increment.

And that's how we do that. We set a buffer called output, 80 characters. And then the time object has a structured time formatting element to format a string output in the character buffer output that we just configured.

And it's 80 characters. And these are formatting functions like printf or sprintf, but they apply specifically to our time library. And we're going to get that data out of the time info structured variable that we updated up here.

And so this is formatting a text string of the data in time info in this buffer of characters. So it's like text. And you can format that in many, many ways.

And that's all specified in the documentation for the time library. And here we have our serial port, our streaming operator, and output. Well, output is that buffer.

And so we formatted a text string in the output and streamed it out the serial port for display. But understand we only do that once per second. And that's exactly when the second ticks over.

Now, time info, we're like 1,000 milliseconds in there, a million microseconds in there. But we're only interested in second. At the point where it increments to the next second, we're going to print this out.

The rest of the time, this does nothing. We're not doing anything with this. It doesn't take any time.

It just has to do this comparison. And if it fails, it just goes on. We have a function called check for input.

We have a function called check for can one. And we have this function to send a can frame. If we've had an interrupt and this do 707 send has been sent, then we will send a can frame.

And so that's our loop. And we just keep looping. And most of the time it's resetting our watchdog, updating our time structure, checking if we've sent anything in the keyboard, check if we have any incoming can on can one, which by the way is redundant and unnecessary.

I don't even need this line. And then we check to see if we've set a flag. And our hardware timer is indicating it's time to send the can frame.

So that's the end of our loop right here. And this will run forever until we remove power from the machine. Our program functions.

Here's a neat one. It's called initialize can. And initialize can is, as I said, it's up here in our setup function.

And I wanted to put off that description because this is all trivial stuff. And initialize can is the purpose of this tutorial video. This is what we're about.

And so we have two can ports we're going to initialize. Can one is the can port that is our second can port that has the new chip, the NCP2517FD. And can zero is our native port in the ESP32 chip.

But of course we had to add a transceiver to send that, to actually manage the physical protocol interconnect for the controller that's in the ESP32 chip. And so there's a bit of hardware, or actually some hardware to implement a two can port thing. And this is our library makes it easy to configure all that.

Let's go to the can one begin. That's actually the one we have set up. And this is a call, a function in our library.

And so we basically extended the C++ language to have this command because of our library and the objects in it, methods in it. And one of the methods is begin. And we're going to specify that it be can one.

That tells us which port and begin is what we're going to do to it. And this is the speed, 500,000 bits per second. And so that can be anything we want between 33 kbps to a one megabit.

And I'm going to print out the serial port using my streaming character, just using that we're using can one. And that's the, which can this is. And I want to remind it's the 2517.

And that the initialization has been completed. If this returns a one indicating success. If it doesn't, we're going to print can one initialization sync error.

And go on. Once we have initialized it, here's where we get into some magic. And that is the, and this is enabled by our libraries.

But it's also part of the chip set. And it's a very powerful way of using can. Can is a bus that might have 30 devices on it.

And they each simply blindly transmit stuff. There's no real request to send and a reply. And they're not really talking to each other.

Every device on the can bus simply spews out can messages. Now, if you're a device that can use the information from that other device, you watch for that can message and you know what to do with it. You know what the stuff means is in the eight bytes.

So you might have one device that spewing can messages. There's only one other device out of 30 that even cares about that. And so it's kind of a rude protocol, but it works pretty well that way.

But the basis of it is all the messages your device doesn't care about and doesn't know what to do with, by using filters, it doesn't even see them. You don't spend any of your processor time receiving and examining messages you don't know what to do with anyway. And we do this by setting filters.

And callbacks are how we handle those messages. And so here I say can one set RX filter 0, 0x107, 0x7FFF false. And what that tells me, we can have up to six or seven filters on a can port.

And I'm going to use filter number zero. And this is the message that it's going to let through. Now the only messages it lets through are 0x107.

However, that is modified by a mask. And here is the mask 0x7FF. And 0x7FF is kind of interesting.

That is really a 0x048 10-bit, 11-bit mask. And by setting this to false, we're saying not extended message IDs. The can has two size message IDs.

Extended is 29 bits. And the normal ones are 11 bits. And so I'm saying 11 bits with false.

My mask is 11 bits. And here is my 11-bit ID I'm looking for. And so what we do is take the incoming messages.

We compare all 11 bits here. Because I've got this set to 0x7FFF. And if all 11 match the 11 bits in 107, the message ID, we have passed the filter.

And we do the callback for that message. Now I can change this to 0x7F0, for example. And instead of looking just for message 107, it will qualify any 10 digit.

It will do 100, 101, 102, 10A, 10B, 10C. Any message that starts with 10. See, I've taken out these four bits.

They've set to 0, so they don't count when I mask it. If I set this to 0x700, then any 100 series message works. 100, 110, 120, 127, 130A, 14F, 1A, 2, anything that begins with a 1. Because I'm only using the mask on the three bits at the beginning.

And if I set this mask to 0, it does an idiotic thing. It's going to pass all 11 bit messages. And if I set this to true, it will pass all 29 bit messages.

11 or 29. And so you can see that we're both setting a filter for a message, but then we're defining a range of that message, from all messages to just the first digit and whatever messages, to the first two digits to whatever messages, or that you have to have all three digits correct. And 7FF, not GG, there is no GG.

Again, this is, of course, hexadecimal numbering. And so 7FF would be the equivalent of a mask that looked like, let's see, 421 would be 111, is 7. And then 1111 is F. And 1111 is F. And so if you wanted to fill this out, it would be 0111. 1111 is a full mask.

And when I'm turning this F to 0, I'm simply turning these rightmost bits to 0. And so they don't qualify in the mask function. They're not used. And we have 11 bits, because that's what the CAN message definition is for that.

I'll leave that little comment there to make that clearer. If we pass the filter and have a message 107, I can set an interrupt for that and call a function specifically to handle 107 frames. Now we're in our loop, and we're looping up here.

And we receive an inbound CAN message off the CAN bus. Our controller chip, the 2517 in this case, in this case the native 40, is going to stop the program function and go execute the function to handle 107 frames. And so that's the deal.

Now here I set, that was for filter 0. For filter 1, I'm going to do the same thing for 707. And oddly, I'm going to send it to the same function that handled the 107s. Now I can do that, or this could be a different function specific to 707 messages.

This is just kind of by way of example. CAN1.watchFor is a function to watch for any message. And so if I get a 107, it's going to go here.

If I get a 707, it's going to go here. And if it fails, both those CAN1.watchFor can catch any other messages. And since I don't have anything defined as what I'm watching for, it'll do anything.

And it'll recognize that. And I can go back later and pull for that. And I'll show you how to do that here in a little bit.

Here's the same structure for CAN0, our native thing. I'm saying for message 110, go to handle CAN frame. 120, go to handle CAN frame.

130, go to handle CAN frame. If I'm doing all these messages to handle CAN frame, then 707 or 107, I'm saying handle 107 frame. But these functions could be entirely different.

They can be anything I want them to be. I just have to write the function to handle that frame. And then again, our CAN1.watchFor, catch all.

And so that handles the CAN. Not necessary to do this watch for. This is just if you want to look at other CAN frames that are on the bus.

If you don't do that, you're only going to look at 110, 120, 130, 140, 707, and 107. And any other frame on the CAN bus is going to be completely ignored. By that I mean your controller doesn't even see it.

The CAN controller does, but your multi-controller, the ESP32 chip itself, is oblivious to its existence. It's not going to do anything with it. It's not even going to see it.

And this is a key function of CAN that's kind of cool. In that any device on the bus can look for any message from any other device, but it's very easy to completely ignore all other traffic. They require no processor time to process, because you're not even going to see them.

The CAN controller is going to check and do that. Now here's check for CAN. I said that this callback is an interrupt callback.

We don't have to use those at all. In fact, if you have something that's flooding you with messages every 10 milliseconds, you may find your program runs a little choppy because it's constantly being interrupted by incoming CAN frames. And so here's an alternate way of handling CAN frames, and it works with this CAN0 watch form.

And that is a polling technique. We're going to say if CAN0 is available. If nothing's available, we're not going to do anything.

That's the end of check for CAN. If there is some available in the buffer, we're going to go say read it, and read in a frame, in the frame structured variable. And then we're going to set up an integer called ID and say let's load that with the frame.id. Our frame structured variable lets us access just the ID of the incoming frame and put it in here.

And then we're going to switch on this. And I can say case 108. That's for CAN message number 108.

I want to go handle CAN frame. That's a function that I've set up. And then I'm going to pass it the address of the frame we just read.

So it can operate on it. And then break. And then default is, you know, serial print unknown CAN frame.

And here's the ID. And we just print that. So I can have 100 cases with 100 different functions that it calls.

But this isn't an interrupt. It's I'm in my loop. I go call this.

And I poll to see if I have any CAN. If I do have CAN, then I have this switch on frame ID to determine which function I want to call based on the message number of the frame. And so this is the non-interrupt way to do it.

And here's one for CAN 1. If CAN available, CAN 1 read. Again, we set the ID and we switch. Here I'm saying case 707 or case 107.

I want to go handle frame and frame. And then break. Our default is to simply print received unknown CAN frame and print its ID in hexadecimal to the screen.

Here's my handle CAN frame. This is a generic thing that will take the pointer to the address of a frame. And all it does is call another function called print frame and pass it the frame.

And this zero indicates it's a received frame to the print frame function. And it's only going to do that if I set the debug variable to 1 or true. So with debug off, handle CAN frame doesn't do anything.

It just returns. With debug on, it prints the frame to the screen. Here's my handle 107 frame.

Again, it will take the address, receives a pointer to the address of the frame that we've received. And then it's going to say received equals frame data dot low. And that's the name of this frame.

This is a pointer to it. And the data structure is low. Well, that's the lower or the first four bytes of the frame.

And we're going to talk a lot later about data structures for CAN provided by the ESP32 because it's kind of the central feature, aside from the callback interrupt, of this CAN library is the way to do that. Now received, we're not really defining what it is, so I'm going to guess I've got it up here in my global variables. And here it is.

It's an integer, the default integer for the Arduino is a 32-bit integer. And so I've already got that as a global variable. And here I'm loading the lower four bytes of the received CAN frame directly into a 32-bit variable.

Well, a 32-bit variable, of course, is four bytes. And it so happens, the way we pack them in the CAN messages is in the right order for the 32-bit integer in Arduino. And that is least significant byte first.

So the least significant byte is the first byte of the frame. We refer to that as byte zero. And then one, two, and three comprise four bytes.

And since there's eight bytes in the frame, we're calling it the low side to designate the first four bytes in the frame. And we can actually load this into a 32-bit integer. We don't have to do a lot of jumping through hoops and shifting bits and multiplying by 256 and all this to accurately get that data into this variable.

Now, most of you know what I'm talking about. But those that are familiar with CAN programming, you do a lot of stupid things to get the data out of the CAN frame in the right order and multiplied by the right value into a simple variable. We don't have to do that.

We can load those four bytes into that 32-bit integer, and it's going to come out just perfect. So we load it in there. If we are in debug, we do a bunch of stuff.

If we're in a little debug, we do a bunch of other stuff. And if we're not either of those, we don't do anything. We've loaded the frame into received, and we reset our watchdog because some of this could take some time.

And that's how we handle a 107 frame. If we are in debug, we're loading into a buffer, getting some times for timing so we can show when it was received, structuring it into a buffer, and these are like each byte. Here's the frame ID, the frame data.uint8. If 1, that is an 8-bit byte.

Number 1, number 2, number 3. We did 0 first, of course. 5, 6, 7. This is 0 through 7 individual bytes are loaded into these formatting placeholders for our sprintf command that puts all this in a buffer, a 300 byte buffer. And after we have basically unwound our entire canned data frame into that buffer, then we call hours, minutes, seconds, milliseconds, the difference since the last one.

And we set this to this diff each time we come in to microseconds since the last one. And so we can then print that. Here's the variable and this ends our streaming.

Now you see why I like streaming. Here's some text. Here's a variable, our buffer, and now here's some more text.

Slash n is a character term. And then I'm going to load time info into our time structure some from 0, 1, and 2 of our frame data is the hour, minute, and second from our time structure. So we're going to, this is the received time from, which is held in frame 0, 1, and 2 is the hour, minute, and second that we received from the other machine that we're connected to.

We're going to put that in the structure time variable time info 2 as opposed to time info 1. And then milliseconds and then we're going to print that out as a time for the received time. That's a big long string showing everything in and about our received CAN message. Now that takes some time to print, so we do that and debug.

If little debug, if you recall, if I put a lowercase d instead of an uppercase d, we did a very abbreviated thing. And if we were receiving a message, we print a period and then the seconds and the milliseconds. We don't print anything that's in the CAN message.

I just want to know that we received a message designated by the period and at what second, millisecond I received it. I don't even put a carriage return. If I have greater than 10 of them, then I'll reset it to zero and send a carriage return.

But so instead of all this stuff in the frame and about the frame, I just want to know I received one and seconds and milliseconds at the time that I received it a time line. And should I print a carriage return? And then I reset the thing. So that's all that I did about handling a 107 frame.

Here, recall, we like to send 707 CAN frames. And so if time of day is valid, if I don't have a good time of day, why am I sending out time of day on our CAN message? But if I did, I get my local time updated into our time info variable. And then we set our out frame.

We're going to set up a CAN frame, but we're going to call this out frame instead of just frame. We're going to set the ID to 707. We're going to set the length to 8. All these commands, this data structure, is from our ESP32 CAN library that Collins does.

We set whether it's extended or not. This is a unusual thing. RTR is a request to transfer, transmit request that you can put in CAN messages.

I've never seen anybody use that for anything. But it's possible they do. We just never do, and we're going to set that to 0 here.

And then here's our 7, 8 bytes. We're going to set the first part to the hour from our time info structure that we just updated up here. We're going to put the hour in the first byte, minute in the second, second in the third, number of milliseconds as a 2 byte integer in here.

And then that would be 3 and 4. 5 we won't put anything in. And 6 and 7 we'll put minutes and seconds. Just a value.

That's our time that we've been running using some functions. If the port is 1, we're going to send it out port 1. Or if it's not, we're going to send it out port 0. CAN 0. And we're going to send the frame, out frame. So we're filling in a form, a structure, a CAN structure with different data.

That's what we're doing here. We're filling out a form for a telegram. And then here we're sending the telegram, out frame.

And so if debug's on, then we're going to print the frame. And if little debug's on, we're going to print instead we're going to print an asterisk and the time mark. And again, if we get 10 of them, we're going to send the carriage return.

And so we can either when we send the frame, we can print nothing. We don't have to print anything. We can, if we have debug set, we print a big, long, ugly thing about everything on the CAN message.

And if it's not debug, if it's little d, or lowercase d for debug, then we're just going to print a very short thing designating it as a sent frame with an asterisk and a time stamp. What time did we send it? We sent a frame and what time we sent it. I'm going to set do 707 send to false.

You know, so we set do 707 send with our hardware interrupt. And if we don't reset that, we're just going to keep sending that frame on every pass through our loop. But if I reset it to false, we won't send it again until we get another interrupt that sets it.

And then we're going to feed our watchdog. Okay. So that pretty much wraps up our CAN description.

I want to walk through just some generic things. This promises to be the longest, most techy video in history. I can't imagine anyone actually wanting to watch this.

But if you want to learn to code in C++ for the ESP32, maybe this works for you. I can't picture it. Here's our check for input function.

Recall in our loop up here, just one of our functions was the check for input. And that's the check for keyboard input on the serial port. And I can simply say if serial available, do the following.

If it's not available, that's the end of the routine. We return to the loop. If you don't enter anything on the keyboard, this is a pretty low maintenance, low time consumption function.

It checks to see if there's serial available. If there isn't any, it returns. But if there is, we're going to set up an integer in byte.

And we're going to read the character in from the port. And then we're going to switch based on the byte. If we have a question mark, we're going to print a menu.

I've actually got that commented out right now. That's what we would do. In the case of D, a capital D, we're going to set debug to not debug.

Well that's a Boolean function and we're just going to toggle it. So you to enter debug, you enter capital D and to turn it back off, you turn it off. And we've already seen what we use that for.

That's to print a big, ugly, extensive data about our can messages. A little lowercase d, on the other hand, toggles little bug. And that's what it does.

If we enter a lowercase i or an uppercase I, we're going to go do another function called get interval. And if we enter a lowercase w, we're going to do a function called get time mark. And oh, well there you go.

Here's get interval. And it says enter the interval in milliseconds between each can frame transmission. And while serial is available, as long as it's available, then we're going to sit here.

And then we're going to set up a integer v and say that this equals serial dot parse integer. And this is a function that will parse the string that came in the serial port and convert it to an integer and store it in variable v. If v is greater than zero, then we're going to print that out on the screen. Serial v equals can frame interval.

And then we're going to set our timer alarm right. My 707 timer to v times a thousand. And so what we take for an input is a 10 and we'll set a 10 times a thousand to be the timer alarm right.

And that will change our period on our hardware timer to whatever we enter. If we enter the five, it would be five times a thousand. If we enter a hundred, it would be a hundred times a thousand.

And that is the number of microseconds in our hardware timer. And so that's get interval. Let's see, get time mark.

Nope, that isn't it. We don't want that. Where is oh, w. Yeah, get time mark.

Well, get time mark, we did at the beginning of our setup. But if we enter a w on the keyboard, we're just going to go do that again. Exactly the way we did it, using the same routine as we did when we first began setup.

And so that's how I can repeatedly log on to or connect to the host router and check my receive signal sensitivity. And so that's all the functions we're going to look for. If we type anything in one of these, we don't do anything.

But that's our check for. And then we've got really, oh, here's print frame. You know, this is, we've already seen this, this is just formatting the thing.

Here is some kind functions for milliseconds, seconds, minutes, hours, that simply are based on our clock, our runtime clock millis is simply the number of milliseconds since power on. And we're doing different math functions on there to return hours, minutes, seconds, and milliseconds. And then we use that for time marking elsewhere in the program.

So that is our entire code for the program. Arduino tells us that we're using 563,754 bytes or 43 percent of our program storage space. The maximum is 1,310,720 bytes.

So we've got, well, over a megabyte of program storage space. And again, when you eat that up, it's not so much with our code, but with the libraries that we have to include to do some of these functions. And then global variables use 39,560 bytes or 13 percent of our dynamic memory, leaving 255,352 bytes for local variables.

And so the maximum is 294,912 bytes for local variables. That's, again, pretty generous, and one of the things we like about this chip is the large amount of program storage and a large amount of variable storage that gives us for programs. We can have fairly extensive programs in the chip, and it all works pretty well So that is our code description.

But wait, there's more. And what we need to talk about next is what we alluded to in here in several places, and that is our data structures for the ESP32 CAN library. And those data structures are really the magic in that ESP32 allowing us to deal with common variables, 8-bit, 16-bit, 32-bit, even 64-bit variables in our program, and load them more or less directly from our CAN message traffic.

So let's go take a look a little bit at data structures for CAN. Okay, let's get started. talk a little bit about the ESP32 CAN data structure.

This is kind of where the magic goes on. Colin and I have worked back and forth on this data structure quite a bit. We have some housekeeping functions.

The biggest one is our ID. It's a 29-bit field, but we can put 11 bits in it, and we designate which one is appropriate by the outframe.extended byte. The outframe family ID, I don't think you're ever going to see anything or any use for that.

RTR is the request for transmit. I haven't seen any use for it. Outframe priority might be useful, but it's mostly handled internally to the library as to which frame would go first.

Outframe extended again defines whether you use 11 or 29-bit IDs. Outframe time. This is a time stamp for received data and four bytes.

It could be interesting for some use. It could have been interesting for what this program did, but I was more interested in when it was available to the program than when it was received on the bus, and I talked about length. From that point, we have a data structure.

These are all housekeeping fields that you can access in your program. You can call for whatever you called the frame, outframe or inframe or whatever you called the can frame structure object, .id, .fid, .extended, .time, .length, you can access all those, but they are kind of environment variables. The data goes into a data structure, and for the structure is eight bytes or 64 bits, and it's the trick is the basic can libraries give you eight bytes to work with.

There's a lot of other ways of accessing that same space. We can, for example, call it outframe data integer 64. Zero would mean that we have one 64-bit integer, and that's our eight bytes.

In the case of fd, we can have through 63, and have 64 integers, 64-bit integers. We can really have 64 bytes instead of eight, but you get the idea here. So, a 64-bit would be eight into 64, it would be zero through seven integer 64s.

But for your basic frame, we just have one, and that is a 64-bit integer, eight bytes, designated zero. That same space could be viewed as two 32-bit integers. An integer 32, data dot int 32 zero, or data dot int 32 one.

That's two 32-bit integers would divide up the same space. And so we could have a program statement, I think we did receive time, receive data or something, that we set to a data dot low or data dot high as an alternate way of referring to a 32-bit integer zero or one. And in fact, that can further be broken up into a data dot integer 16 or 16-bit integer zero, one, two, or three.

And so there's four of those 16-bit integers. So we had one 64-bit integer, two 32-bit integers, or four 16-bit integers. Or we can refer to it as data dot int eight, zero through seven, and that's your eight basic items.

There is another way to access this, and I like it because we often deal with frames that every bit is simply a status bit in a vast error field or something. And so this lets us access individual bits in the same space. Outframe data dot bit zero through 63 lets me designate any one bit in that frame and deal with it, set it to on, set it to off, or load it into a variable.

And so I can access it that way. So in this way we can map can, the eight bytes that are in a standard can message using these structure designators, we can load a 64-bit integer or two 32-bit integers or four 16-bit integers, eight 8-bit integers, or 64 bits, individual bits, programmatically from within our thing. And it will all go out as eight data bytes.

Of course, by changing the length, you don't have to have eight bytes, you can have one byte, four bytes, five bytes, whatever you want, but you can still access the data using this format in the text of your program to more easily access individual bits or bytes and map them to integers, typically, in your frame. You could have a floating-point variable, for example, that you set out frame dot data dot integer 64 sub zero equals a floating-point variable in your program. And it would simply map that floating-point variable into the eight bytes in the correct order and space.

And at the other end, you could say this floating-point variable equals in frame data dot 64 zero and load it correctly in. There are a huge number of manipulations normally in your program to, well, if you have a least significant bit and a most significant bit and a 16-bit integer, you'll say byte zero plus byte one times 256. And that is just to load it into an integer.

You multiply the second, the eight most significant bits by 256, and then add the least significant eight bits to that to get your value. You don't have to do that. You can just say this 16-bit integer equals out frame dot data and 16.

Now, if you're not using LSB-MSB in your program, or the way you're storing it in the CAN message, then you have to go back to doing those kinds of message But if you're using standard LSB-MSB convention, we can map variables directly into our CAN data using this, these designations to access Collins data structure for CAN. And that makes it programmatically much easier to deal with and much more readable in your code and move the details of that into the library, where we don't have to deal with it very much. So that's, I think, one of the most compelling aspects of Collins library is the CAN data structure accessing tools that you have with the CAN.