Most e-Scooters and other Electric Vehicles have some form of Electronic Speed Controller (ESC) which is responsible for delivering a power signal that will spin motors at the desired speed. The most common type of motors today are of the BLDC type (Brushless DC motors), which are typically driven by a 3-phase current.
For these BLDC motors, the ESC needs to be able to generate an accurately timed 3-phase signal that varies depending on the position of the shaft and the required torque. It is therefore a relatively complex device which besides having to handle large amounts of power, it needs to perform very fast switching of the current and operate in a closed loop (1).
Electric scooters normally have a display unit (LCD), a trigger or thumb throttle, and the actual ESC's that drive the motors. This LCD unit and the ESC communicate with each other in order to exchange data. The LCD unit needs to transmit data to the ESC so that it can configure it. On the other hand the ESC needs to provide status information to the LCD unit so that it can be displayed for the user (e.g. show the speed, current mode of operation, etc).
In the sequence of the previous post, I took the challenge of figuring out what at a first glance appeared to be a somewhat cryptic protocol, full of random data, as found in my Speed Controller from the Jipin manufacturer (J&P).
I took the precursor work of the user raxrip, posted here:
And realized that the frame size and overall structure is very similar to that generated by the QS-S4 LCD units.
After building a couple of python scripts, which I have pushed to this git repo:
I was able to decode most of the data, both for the frames generated by the LCD, and those generated by the ESC (speed controller).
All of this is detailed in this repo, but in a nutshell, I was able to figure out that the LCD unit sends 5 of the 10 P-settings that are configurable, and these are structured like this in the 15 byte frame:
B00 (01) - Fixed B01 (03) - Fixed B02 (00) - Sequence (00, 01, 02, ..., FF) B03 (00) - Fixed, always reads 0x00 B04 (00) - Fixed, always reads 0x00 B05 (85) - Not entirely random: pairs of consecutive frames alternate between having consecutive values and not having. B06 (00) - Contains the following flags: b000000x0 - pedal assist (P05 setting: x = 1 -> on; x = 0 -> off) b00000x00 - cruise control (P06 setting: x = 1 -> on; x = 0 -> off) b0000x000 - soft start (P07 setting: x = 1 -> on; x = 0 -> off) B07 (46) - Fixed (P08 setting -> 0x46 = 70%) B08 (00) - Fixed, always reads 0x00 B09 (80) - Fixed, always reads 0x80 B10 (02) - EABS (P09 setting: 0x00 to 0x02) B11 (00) - Fixed, always reads 0x00 B12 (00) - Fixed, always reads 0x00 B13 (00) - Fixed, always reads 0x00 B14 (43) - checksum (XOR of bytes B0 to B13)
Byte B05 contains a seemingly random value, but I suspect it is where the gear information is encoded, as I could not find it elsewhere in the frame. Changing gears in the LCD unit does not condition the throttle signal sent to the ESC, so that there aren't many options left but to somehow be encoded in this frame.
Regarding the frames sent by the ESC, these are also 15 bytes long. I could determine most of the fields, which can also be decoded by one of the python scripts that I have added to the above mentioned repo. These have the following structure:
B00 (36) - Fixed B01 (19) - Sequence (00, 01, 02, ..., FF) B02 (00) - Fixed, always reads 0x00 (padding apparently) B03 (5b) - Entropy value (probably adopted as an extra measure to avoid framing errors). The values in B04, B05 and B07 to B13 must be subtracted from this value to obtain the payload. When value smaller than B03, add 0xFF. B04 (7e) -
Subtract B03.Contains the following flags: b000000xx - Turbo (xx = 11 -> on; x = 00 -> off) b0000x000 - Regen (x = 1 -> on; x = 0 -> off) b00x00000 - Brakes (x = 1 -> on; x = 0 -> off) B05 (5b) - Subtract B03. Always reads 0x00 B06 (00) - Fixed, always reads 0x00 (padding apparently) B07 (5b) - Subtract B03. Proportional to wheel speed, most significant byte. B08 (5b) - Subtract B03. Proportional to wheel speed, least significant byte (multiply by 1.33 to obtain speed in RPM). B09 (5b) - Subtract B03. Power/motor current most significant byte (?). B10 (65) - Subtract B03. Power/motor current least significant byte. Minimum value 0x0a when throttle pressed. B11 (6e) - Subtract B03. Usually reads 0x13. Battery voltage? Temperature? B12 (e3) - Subtract B03. Usually reads 0x87 or 0x88. Battery voltage? Temperature? B13 (5b) - Subtract B03. Always reads 0x00 B14 (b9) - checksum (XOR of bytes B0 to B13)
Regarding the wheel speed in particular, I determined empirically that the value reported is proportional to the RPM measured directly via an optical tachometer.
Multiplying this value by 1.33, we obtain the correct RPM. Given the fact that the LCD supposedly uses the parameter P03 from the P-settings:
P00 - Wheel diameter: 11 P01 - Voltage cutoff: 51.1 P02 - Number of magnetic pole pairs: 15 P03 - Signal selection (read only): 1 P04 - Distance units (Km/h = 0 or Miles/h = 1): 0 P05 - Pedal assist (off = 0, on = 1): 0 P06 - Cruise control (off = 0, on = 1): 0 PO7 - Soft/hard start (off = 0, on = 1): 0 P08 - Performance (0-100%): 100 P09 - EABS (0-2): 2
I can speculate that internally it will use it to calculate the coefficient that is then multiplied by the value reported by the ESC. The result is then used to calculate the speed in Km/h, based on the wheel diameter which is also stored as a P-settings (P00). Using this coefficient and the same wheel diameter, my python script returned the speed matching exactly the speed shown in the LCD display.
The frames are sent from the LCD to the ESC at a rate of about 5.26 Hz:
On the frames that originate from the ESC, these occur at 2.4 Hz:
For tapping both directions of the serial communication at the same time I used two separate serial port to USB adapters. I made a very simple cable which taps into the TX and RX pins, and in order not to affect the line impedance too much, I have added a 1K Ohm resistor between the tap and each line.
The circuit boils down to what is shown in this diagram:
Having achieved the decoding of most of the data in these frames gives room for some creativity. One possibility can be to implement a man in the middle device (e.g. an ESP32 board) which in a default mode of operation will just forward the data to the LCD or ESC depending of where it came from, and in another more complex mode of operation, be able to manipulate some of the parameters.
Assuming that the byte B05 from the frame sent by the LCD is responsible for signalling the crossing of the speed limit, by manipulating this field one could effectively have better control over the speed cap of the scooter. For example in my scooter, the Eco mode respects the typical legal limit of 25 Km/h of a vehicle of this type. And in this mode the throttle has a very smooth curve thorough the entire travel range of the lever. On the other hand turbo mode has a very different type of response. If the user selects a low gear, in spite of the speed being limited accordingly, the throttle is still very sensitive, and only provides proportional response in small range of the lever. The assumption behind manipulating this frame would be of having the best of both worlds: we would preserve the same throttle curve of Eco mode, but not cap the speed when it reaches 25 Km/h.
Another possible feature could be to allow some management via a smartphone app: because the ESP32 also has a Bluethooth BLE interface, it could be an interesting addition, where we could control settings via the app, or monitor and log the speed, power, brakes status, regen, etc, given the fact that we would be tapping into it.
I will focus on trying to understand this "strange" byte in the frame, and see if it is possible to decode it and figure out how the ESC is instructed to limit the speed of the scooter, once the threshold is reached.
1- Normally in order to estimate the position of the shaft, some kind of resolver is required. One of the methods consists of measuring the output from the unpowered phase. When one of the stator coils crosses a magnetic pole, a current pulse can be measured, therefore providing feedback on the motor position. This is not however a very reliable method, as when the motor is stopped, it is not possible to determine its position. This is because there is no induced current when a magnet is stationary relative to a coil. BLDC motors using this approach will normally start "blindly" with low torque, and as soon as a signal is picked up from the unpowered phase, these will readjust the timings and continue at the desired torque. This method is good enough for applications that don't require full torque at zero RPM (e.g airplanes, helicopters, boats), but is inadequate where motors need to drive wheels and provide torque from a stand still. In this case an external sensor is required. The cheapest and most common approach used in e-Scooters and other EV's consists of adding Hall Effect sensors to the motor stators. Depending on where the magnetic poles are located relative to these sensors, a very linear response is obtained. This provides a very good position estimation, regardless if the shaft is moving or not.