Ego Z6 Autonomous Mower Conversion

I’m working on a same conversion and got a few questions please.

  1. Is it possible to handover the serial bus communication to a dedicated serial bus transceiver? It seems not trivial to use the IO_MUX in order to achieve the inverted part on D- ? By searching, I mostly find CAN bus transceivers like TJA1051, however, they use the arbitration ID to avoid bus collision, which seems not the case on Z6.

  2. Talking about bus collision, is it possible to trigger a send on an rising signal of the CC line? Tbh, I haven’t started tapping into Z6’s bus yet, but just curious how it handles the frequency each module occupies the bus. E.g. From the code, Martin seems to decided sending the emulated messages at 1hz, while testing on my bench the hall sensor module sends its message at 500hz. Let’s say when all the modules are connected to the bus, what mechanism is used to make sure each module gets a fair share?

I don’t have the skill to answer your questions, but am curious if you are deciphering the EGO bus messages. If so, do you have a list of command codes? I am in particular interested in the battery SoC, which would allow the mower to auto-charge and return to the place it stopped, continuing the mission.

I haven’t started deciphering the bus messages yet, but will soon begin.

My plan is to start with the battery box and identify the messages emitted from it, and then one by one connect other modules and discover new messages.
That being said, I’m not EE major and the manual for esp32 is quite challenging to read tbh. E.g. It took me a couple of days to figure out how Martin was able to output inverted signal on D-. And that’s why I was asking if there is any easier route to produce the balanced signal, or handling the bus collision, even though esp32 is totally capable of doing it.
(https://www.espressif.com/sites/default/files/documentation/esp32_technical_reference_manual_en.pdf#iomuxgpio)

Will probably open a new post once I got something worth sharing.

CAN bus transceivers aren’t suitable for this application. The differential voltage isn’t large enough. The output needs to swing 3V on both D+ and D- for the Z6.

Collisions are detected by checking the CC line before transmitting as this is active during transmission. All messages are the same length, and all modules transmit at fixed intervals so if a module wants to transmit when the bus is busy it just has to wait the fixed message time before transmitting and then continue with it’s fixed interval.

You are correct that the hall sensor message frequency is ~500Hz (the left an right sensors are slightly different). I’m sending at ~1kHz because I’m emulating both sensors. The confusion is because I put milliseconds in the comments when I meant microseconds. Max pointed this out to me a while ago.

The battery box messages have the header 0x09 and the battery percentage is in byte 7.

1 Like

I hope I’m not stepping on (Martin’s) toes… in order for me to comprehend, or at least trying to, I took Martin’s code and ‘expanded’ the variable names. Maybe it helps others to better understand the program. While I have done this six months ago, and bought the hardware, I haven’t had time to put the hardware together and test my changes.
Once I have had time, I will post either the confirmation that it works and/or the code if it required changes. (Don’t hold your breath time wise.)

main.cpp (19.3 KB)

Thanks Martin for the answer.
It’s quite interesting how Z6 chooses to use a set of different frequencies to handle bus collision. It’s like the priority of a module is decided by its frequency and therefore the likelihood of it getting lucky and fit into a gap on the cc line. Maybe it’s simpler to implement than the traditional CAN bus ID arbitration method. I’m thinking instead of copying this lucky-to-fit mechanism, can I use interrupt on a CC line rise-edge to fire the emulated hall messages? Feel it’s more predictable that way.

Thanks Max for the code. Will take a look shortly.

There’s not really any luck involved. Since all the module send messages at a fixed rate, once they have all moved out of each others way collisions are rare and only occur due to drift between modules.

This arrangement works very well and has the advantage that no module is master so they can all transmit independently. Triggering off the CC line would imply that one module was master and wouldn’t work anyway for the hall modules as they transmit much more often than the other modules so there wouldn’t be anything to trigger off most of the time.

1 Like

I see. Apparently I was under the wrong impression that all modules are sending messages at roughly the frequency of the hall sensors, which I thought there was not much space between each messages. I haven’t got time to decipher all the messages from z6 yet, and thanks for the clarification.

As per the cc line trigger, what I was trying to say was either send the message if the CC line is high OR if it is busy then trigger on the rising edge. I mean, given most of the modules send at a low frequency and the collision is rare, I guess it’s not necessary.

Ah, ok, I see what you mean now. That would probably work but as all the messages are of a known fixed length it’s easier to just shift the timing by slightly more than the message duration.

I have some silly questions.

If I understand it correctly, to generate inverted signal on D- (pin 19), it goes like

connect pin 21 to serial1 TX
Serial1.begin(EGOBAUD, SERIAL_8N1, -1, EGOBUSPPIN, false);

pin 21 outputs to signal 17
REG_WRITE(GPIO_FUNC17_IN_SEL_CFG_REG, EGOBUSPPIN | GPIO_SIG17_IN_SEL);

signal 17 outputs to pin 19 with inverted set to true
?????

send regular signal to pin 21 and will see inverted signal on pin 19
Serial1.write(message, MESLEN);

From the code, I guess the ??? part is done via

#define EGOBUSNFUNCREG (DR_REG_GPIO_BASE + 0x530 + 4 * EGOBUSNPIN)

and

REG_WRITE(EGOBUSNFUNCREG, SELUART | 0x600);                          // and invert bus D-

however, maybe I missed it, I can’t find how it’s explained in https://engineering.purdue.edu/477grp4/Files/refs/esp32_technical_reference_manual_en.pdf

Does anyone know the right place to find the explanation on how EGOBUSNFUNCREG is defined?
like the magic number of 0x530, 0x600 or SELUART = 0x011 ?

On a different note, I was trying to dump the GPIO pin configuration using
gpio_dump_io_configuration

however, it’s seems not an easy way to use stdout as the output stream in Arduino IDE. As a comparison, it works just fine in esp-idf. I only tried a little bit but not sure the added complexity of IDF is something I want to challenge myself right now.

I managed to hacked it with memory stream like

Serial.begin(115200);
	
FILE *memStdout;
char *memStdoutBuf;
size_t memStdoutLen;
memStdout = open_memstream(&memStdoutBuf, &memStdoutLen);  

gpio_dump_io_configuration(memStdout, (1ULL << 4));
fflush(memStdout);  

Serial.printf("%.*s\n", memStdoutLen, memStdoutBuf);  

and able to see the output in serial monitor

12:08:16.320 -> ================IO DUMP Start================
12:08:16.320 -> IO[4] -
12:08:16.320 ->   Pullup: 0, Pulldown: 0, DriveCap: 2
12:08:16.320 ->   InputEn: 0, OutputEn: 0, OpenDrain: 0
12:08:16.320 ->   FuncSel: 0 (IOMUX)
12:08:16.320 ->   SleepSelEn: 0
12:08:16.320 -> 
12:08:16.320 -> =================IO DUMP End==================

The question is for projects like this Z6 conversion, is esp-idf something eventually I’m gonna need to switch to, or Arudino IDE is more than enough?

All I can say here is, I use VScode with PlatformIO (for years), and it works like a charm.

I did a quick capture of messages from Z6 today and found the frequency of each message type. just in case someone is interested.

  • 55 AA 09: battery @ 19.70 hz
  • 55 AA C0: ?? @ 19.60hz
  • 55 AA CC: control module @ 19.77 hz
  • 55 AA C1: front module @ 24.50 hz
  • 55 AA 01: ?? @ 16.7 hz
  • 55 AA 02: ?? @ 16.8 hz
  • 55 AA 03: motor @ 40.12 hz
  • 55 AA 04: motor @ 40.25 hz
  • 55 AA 11/12: hall sensors @ ~500hz

I have also talked with someone with EE background and he suggested an oscilloscope read.
Based on the reading from my cheap oscilloscope, plus I found a 485 chip near the USB-C connector. It is very likely Z6 is using a 485 bus.

One interesting thing is that the tp8485e chip’s datasheet says the supported highest data rate is 250kbps, about half of the observed 470k (Actually, my EE friend pointed out it’s probably aiming at 460800, one of the standard rates). Given the other 485 chips from the same manufacturer are mostly supporting 500k and up to 10M, Ego might have found that tp8485e worked on 460800 just fine and have been kept using it.

I have bought some 485 transceivers and see if I can emulate the balanced signal without going the IO_MUX route.

1 Like

The 01/02 messages are filtered versions of the control lever positions.
byte 4=1 if the lever is out, 0 if it’s in
bytes 5&6 are the lever position 0-720 where 360 is neutral and 0 is full reverse

2 Likes

with a 485 transceiver, e.g. sp3485 from a random seller on aliexpress

I’m able to achieve the same balanced signal.

from my cheap oscilloscope, the signal seems to match what’s captured from the hall sensor.

I mean using the IO_MUX is a perfect working solution as illustrated by Martin, however it requires a good understanding of ESP32 which I don’t have. So, for someone like me, seems like utilizing a 485 transceiver makes the task of outputting a balanced signal a lot easier.

1 Like

Nice. (If I may ask??!) Which logic analyser do you use?

https://a.co/d/9eSNpoj

1 Like

The ESP32 GPIO matrix is a bit tricky to get your head around but it’s all explained in the technical reference manual and it does make the ESP32 incredibly versatile. It only requires one register write to get the inverted UART tx signal onto any pin.

The GPIO output function register is described in section 4:

bits 8-0 specify the function of a pin and these are listed in the peripheral signal list:

We want UART1 TXD so function 17 or 0x11. We also want the signal to be inverted and output enable control via the GPIO_ENABLE_REG so we also need to set bits 9 and 10.
The register address is shown in the register description as 0x530 + 4 x pin# and this is relative to the GPIO register base so…

REG_WRITE(DR_REG_GPIO_BASE+0x530+4*19, 0x17 | 0x600);

…is all we need do.

1 Like

Thank you for sharing your wisdom (which I truly appreciate); I could never have come up with this; chapeau.

One reason why I took your code and expanded the variable names to even have a chance to understand it all (which I still haven’t, though getting there).