moteus r4.11 has two external connectors, the ABS connector (AUX2) and the ENC/AUX1 connector. The ABS connector was designed initially just to have 2 I2C pins. The ENC connector just has the random pins that were used for the onboard encoder SPI plus one more. Thus the range of external accessories that can be connected is somewhat haphazard and not necessarily all that useful.
When working on a more ground up revision of the controller, I wanted to improve that situation to expose more connectivity options on still a relatively limited connector set. The idea was to use 2 connectors, one which has 5 I/O pins and the other with 4 I/O pins. The onboard encoder SPI would still be accessible on the larger connector to use for at least one external SPI encoder, but how much other functionality could be crammed into the remaining pins? To start, lets see what possible options there are in the current firmware and supported by the STM32G4 microcontroller that moteus uses:
SPI: The larger connector by definition would have a set of SPI lines, MISO, MOSI, and SCK (now sometimes termed CIPO/POCI and COPI/PICO).
I2C: I2C requires two lines, one for data and one for clock.
ADC: Sine/cosine encoders and general purpose ADC inputs require analog inputs.
Quadrature: Quadrature encoders require two signal lines.
UART: Asynchronous serial lines can be used for a variety of purposes.
5V Tolerant: While the STM32G4 used in moteus is 3.3V native, it can be convenient to support 5V inputs.
To be useful in the moteus firmware, most of these capabilities need to be accessed through STM32 specific hardware. The one exception is quadrature inputs, for which the firmware can manage slow to moderate rates using interrupts alone, but high rates requires hardware decoding. Complicating this, the STM32G4 only provides access to specific hardware peripherals on specific pins through the alternate function map:
My challenge was to figure out which microcontroller pins to assign to the 9 (5 on AUX1, 4 on AUX2) ports which maximized the number of hardware peripherals that could be used on each connector. There are a few additional twists that make this process more challenging than one would expect.
Multiple STM32 pins per connector pin
It is possible to connect multiple STM32 pins to the same external connector pin. With this, the software for any given user requested configuration can leave the unused pin in a high impedance mode where they will largely not effect the output. There are some constraints with this though, caused by the STM32 architecture.
If a pin without analog functionality is connected to an analog signal, then it has a permanently connected schmitt trigger attached. This will cause undesired behavior and power consumption at certain analog voltage levels. Pins with analog functionality have an additional switch to disconnect this. Thus if a user visible pin is intended to have analog inputs, then all the STM32 pins must have analog functionality.
Similarly, if a connector pin is intended to be 5V tolerant, then every STM32 pin connected to it must also be 5V tolerant.
The analog input pins are sprinkled across the 5 different ADC converters present on the STM32G4. Ideally, the pins would not all use the same ADC, so that the sampling window could fit into the existing ADC sampling time of the main interrupt service routine.
Doing the search
I first attempted to conduct this search by hand, but found that I had a hard time wrapping my head around the possibilities, kept getting lost back-tracking and ultimately could not keep all the constraints in mind at once. So… I wrote a tool! I ended up making a brute-force python script that consumes a simple one-pin-per-line encoding of the capabilities, takes some optional constraints like pins or peripherals to not use, and finds all possible configurations which optimize a metric.
Portion of pinout definition
I used this in two separate phases. First I ran it in a mode on the 4 user-pin connector to find a configuration where all user pins were 5V tolerant. Then for the 5 user-pin connector, I excluded the pins and peripherals used on the 4 pin connector, and added the constraint that the non-SPI pins had to be 5V tolerant. The onboard magnetic encoder also connected to these SPI pins is not 5V tolerant, so there was no reason to aim for that here. On this second phase, there were bonus points in the metric for how many other peripherals could be crammed into these 5V tolerant pins, since they could be used even while using the onboard magnetic encoder.
The tool has a few separate classes for each of the constraints. Each evaluates a pin configuration or subset of pins, and returns whether that constraint has been met, is inconclusive, or is impossible to meet. Enumerating the possible sets of pins was slightly complicated because of the optional “pin doubling” that can occur. I ended up using an encoding of the problem that made this not too troubling.
In the end, I met nearly all of my goals. The 4 pin connector looks like:
Connector Pin
STM32G4 Pin
Functions
1
PF1
5V / SPI / ADC
2
PA10 / PF0
5V / SPI / UART_RX / I2C_SDA / ADC
3
PA11 / PC4
5V / SPI / UART_TX / I2C_SCL / ADC / QUAD_3A
4
PB7
5V / UART_RX / QUAD_3B
The only real downsides here are that if hardware quadrature is used, then neither USART nor I2C can be used simultaneously.
For the 5 pin connector, the following assignment was chosen:
Connector Pin
STM32G4 Pin
Functions
1
PA5 / PB14
SPI / QUAD_1A / ADC
2
PB4
SPI / QUAD_2A / UART_RX
3
PA7
SPI / QUAD_2B / ADC
4
PA15
5V / QUAD_1A / I2C_SCL / UART_RX
5
PB3 / PB9
5V / QUAD_1B / I2C_SDA / UART_TX
Here, the only bonus metric which was not satisfied was having ADC capabilities on the non-SPI pins. Thus to use ADC functionality on the 5 pin port, the onboard magnetic encoder must be disabled.
Conclusion
It probably doesn’t make sense to spend this much time on pin configuration for a purpose built board. In this case, since the number of external peripherals connected to moteus can be relatively large and each end-user may have a different idea of what constitutes a useful configuration, I think it was worth the effort to maximize flexibility of the exposed pins.
With the release of more flexible I/O support, the moteus controller auxiliary port can be used to monitor encoders using an onboard UART. Now, with firmware release 2023-02-01, those UART pins can be used as an arbitrary logic level serial port controlled by the application! Let’s see how to use it below.
First, you will need to look at the pin configuration table to find pins that support UART functionality, and configure them as UART in the “aux?.pins” configuration tree. Next, “aux?.uart.mode” should be set to “kTunnel”, along with the desired baud rate. That’s it on the configuration front.
To send and receive data from the serial port requires using the diagnostic mode CAN protocol. To date, diagnostic mode channel “1” has been used to send and receive diagnostic mode commands from moteus itself. Now two new channels are available:
2: aux1
3: aux2
Unfortunately, tview does not yet have support for these. However, the python library and moteus_tool do. If you are not running any moteus using application at the same time, you can start moteus_tool with “moteus_tool –console –diagnostic-channel 3” to send and receive data from the aux2 serial port rather than the normal moteus diagnostic protocol. Similarly, python applications can use the “moteus.Stream” class and specify an alternate channel, like:
c = moteus.Controller()
aux2_uart = moteus.Stream(c, channel=3)
await aux2_uart.write(b"Data to write on the aux2 UART pins")
print(await aux2_uart.readline())
The moteus controller, when it implements its control algorithms, uses the internal RC oscillator of the onboard STM32G4 microcontroller to calculate things like velocity and to advance position over time. Typically, this is accurate to within 0.5% which is more than sufficient for most applications. However, there are cases where it does matter.
One common case is when multiple moteus controllers are operated together, and either the relative velocities of the controllers must match closely, or the time required to complete long trajectories must match closely. For example, if a trajectory would take 100s to complete, then a 0.5% difference in clock rate between two controllers would result in one completing 0.5s before the other.
As of firmware release 2023-02-01, there is now a mechanism whereby an application can synchronize the clock of a moteus to the host time base. If this is done for multiple devices, then they will all share the same time base and, on average, produce identical velocities and trajectory time-to-completes.
Implementation
To make this work, there are two pieces. First we need to be able to change the rate at which the microcontroller’s clock operates. The microcontroller does provide a trim mechanism for exactly this purpose. At the factory it is calibrated and then the firmware is able to further tweak the result in approximately 40kHz increments, as compared to the 16MHz base RC oscillator frequency. For moteus, that works out to about 0.25% increments of speed. This trim was already exposed as an undocumented configuration option clock.hsitrim, but not in a form suitable for modification online.
So, the first part of this work creates a new register, “0x071 Clock Trim” to which an integer can be written. This integer is an offset to apply to the factory programmed trim, so 0 results in running at the factory default rate, 1 results in running ~0.25% faster and -1 results in running ~0.25% slower.
The second part of this work provides a way for the host application to measure how fast the moteus time base is progressing relative to the host time. This operates through the “0x070 Millisecond counter” register, which merely reports the number of elapsed milliseconds as counted by moteus.
Using these two features, an application works as follows: At a regular rate, it polls the millisecond counter to see how many milliseconds moteus thinks has passed. If that is more than have passed on the host, the moteus clock can be slowed down. If it is less, then the moteus clock can be sped up. Additionally, a measure of the total time elapsed since application start can be used to zero out the overall drift. Doing this in a robust way requires a little bit of thought, but there is an example python implementation in the moteus repository showing how it can be done:
One caveat is that since the resolution of time rate adjustment is relatively large, 0.25%, velocities on multiple controllers may instantaneously differ by approximately that much. Only averaged over time would they match exactly. Secondly, the synchronization procedure is more complex than base control and may introduce other failure modes.
Synchronizing clocks is not likely to be used all that often, but in some cases it can be simpler than other approaches. If it might be useful to you, take a look a the example script above, or drop into the mjbots Discord #moteus channel and ask!
Partly to celebrate moteus controllers being back in stock and partly because a lot of important work has backed up, we’ve just released a new firmware version for moteus (2023-02-01) that has a little bit of something for everyone. Future posts will examine some of these features in more detail, but for now you just get the bullet list:
Support sending and receiving arbitrary data from a UART configured on either of the auxiliary ports
Permit I2C encoders to operate at up to 1kHz
Report control position, velocity, and torque as well as the errors in tracking them over the register protocol as 0x038-0x03d
Provide support for synchronizing the clock of a moteus controller with a host application
Report hall effect errors
Expose the drv8353 error flags as register 0x140 and 0x141
Fix register 0x006 (ABS port position) to be reported in revolutions
While testing moteus controllers, it is often necessary to experiment with high power conditions. For short durations, any decent sized brushless motor can work, as the windings have a non-zero thermal mass and take a little bit to warm up. However, when testing at high power for extended duration, it can be hard to find a way to get rid of all output energy. Even blowing a fan directly onto a motor only gets you so far when you are trying to get rid of 1kW.
Thus enter my resistive dummy load:
This is just a block of DC water heaters screwed into a plastic container. They are wired in series with some high current inductors to roughly approximate the inductance and resistance of a motor in the range of what is normally driven by moteus. When conducting a test, the container can be filled with water to greatly increase the available thermal mass (and if need be boil away the water).
Parts and Assembly
I have used this fixture with two different elements, a 24V 900W one, and a 12V 600W one depending upon what resistance I want to test with:
To assemble, I used a 1.25″ hole saw to cut each of the holes, then used a 1″ NPT nut to fasten each element in place. Each phase connected to 3 of the inductors in parallel in series with 4 of the heating elements. All three phases were tied together in the center to form a wye topology.
Here’s a not-so-brief story about troubleshooting a problem that was at times vexing, impossible, incredibly challenging, frustrating, and all around just a terrible time with the bare-metal STM32G4 firmware for the moteus brushless motor controller.
Because of all that testing, we’re pretty confident to release new firmware images once all the tests have passed, and try to ship out boards with firmware that is within a week or two of the newest on all boards and devices that go out the door. That said, there is some effort made to ensure that large orders all have the same firmware on them. Thus, my saga started when I went to re-program a few dozen boards using the end-of-line test fixture so that they could all match the most recent version.
The first symptom
When I went to re-program them, a large portion of the boards failed tests surrounding the quality of the current sense measurements, indicating there was too much noise in the current sense measurements, specifically when driving 0 current. That could mean that there were soldering problems on the board, or that the test fixture had corroded contacts, or potentially firmware issues. In response, the test fixture got its contacts cleaned very thoroughly, I verified this was happening across many boards all of which had passed earlier, and there were only 3 changesets that affected the firmware in any way, all of which seemed pretty innocuous.
Once I had given up on the problem being a fluke, I opened up tview on the end-of-line fixture and sure enough, wow, there was a problem:
Note how the values of servo_stats.adc_cur3_raw seem to bounce between what looks like their true value and 2048. I have seen problems like this before, related to ADC configuration and clock rate (as – have – others), but absolutely nothing about the ADC configuration has changed in more than a year, so surely that can’t be it, can it?
The first diagnostic step
So, first things first. Now that I can observe a problem, is it reproducible. I used git bisect across the relevant firmware versions, and sure enough, one of the changes was positively correlated with the problem: 64f2a82575795d782ff3806ea2036f4cd2f02ef0 However, that change does absolutely nothing with the ADCs or the current sense pipeline, or the STM32 register configuration at all. So, I tried to create a more minimal version of that change which would still trigger the problem. What I got was this:
So, adding the initialization of a member in a random structure (the one that holds PID gains among others), triggered the issue. If the initialization was only of a uint8_t or uint16_t, no problem, but a uint32_t, float, or uint64_t did it.
Well, “that’s odd”.
Clearly that change shouldn’t have any impact, so if the problem is at the C++ level, it must be undefined behavior somewhere else, and if it isn’t at the C++ level, it could be anywhere. So, my next step was to look at the difference in the disassembly to see what that code change wrought that the STM32 would see.
This is from “meld”, with a set of custom filters to remove most spurious changes related to addresses changing. But yikes, that one extra initialization results in a *lot* of churn in the assembly. If we look at the structure constructor, the change we expect is there in that we can see that the field is getting newly initialized.
However, with “-O3” optimizations on, gcc-11 makes all kinds of different decisions at various points. Instructions are re-ordered, different registers are used, entire blocks of code are re-ordered in their memory layout and execution, and extra padding is added or removed. There are many changes, any of which could be interacting with whatever undefined behavior is in the system.
Taking a step back
Since looking at the disassembly wasn’t going to be easy, I decided to take a step back and see if I could observe what was different in the system when it was running between the good and not-good states. Most likely some peripheral was configured incorrectly, with the ADCs being a prime candidate, but the clock tree could also be a culprit.
When debugging STM32s, I sometimes use the PyCortexMDebug project, which lets gdb use the vendor provided SVDs to interpret the contents of all registers. Here, I wanted to print out every register on every peripheral just to see what was different. PycortexMDebug doesn’t natively give you a way to do that. However, it can list all the peripherals it knows about, which I wrote to a file and pre-processed to remove the human level annotation. Then using gdb’s “python-interactive” mode, I could do a:
python-interactive
> regs = [x.strip() for x in open('/tmp/all_regs.txt').readlines()']
> for reg in regs:
> gdb.execute('svd/x ' + reg)
Which did the trick — at least after copy and pasting the output from the terminal. I didn’t bother figuring out how to get it written to a file. So, now, I have two giant files with every peripheral register, one from a firmware that was working, and one from a firmware that was exhibiting the extra noise. I went through them line by line and found…. nothing.
Some registers were different of course, but the only ones were timer values, and data registers on the ADC and SPI peripherals, and the system control block depending upon if the code happened to be in an interrupt when I stopped to sample it. No configuration values or anything that would point to a problem. Sigh.
More backing up
OK. So maybe there is a peripheral register that isn’t in the SVD that would correlate with the problem? My next step was to use gdb to dump the entire peripheral address space to an srec file in both cases.
Note, this does take a *long* time, at least 15 minutes with the hardware I was using.
What did I earn for my hard earned wait? Bupkis, nothing, nada, squat. After looking through every single byte that was different, the only ones that had changed were the same ones that the svd method above turned up, plus a bit of random noise in the “reserved” section between peripherals that looked like genuine bus noise. Notably, not any configuration registers on any peripheral at all.
Even more backing up
OK. So if the problem isn’t in a peripheral register, maybe there is some difference in program state that is causing the problem? Maybe a stack overflow or something? So, I switched to SRAM dumps. First, I modified my startup assembly to start out with guard bytes across all of SRAM so that I could verify the stack hadn’t overflowed (not even close). I also used that to verify that the code which was copied into CCM SRAM on startup hadn’t overflowed or been stomped on (it hadn’t). Next I did a diff between the working and non-working states.
Here, there were a lot more differences as the firmware has a lot of state that varies from run to run. With the structure of the moteus firmware, most storage ends up being allocated on the C/C++ stack from a fixed size pool. This means that most of the variables don’t have a useful entry in the symbol table, even though their address is consistent from run to run. To identify what each change was, I started the firmware afresh with a breakpoint on _start, then added a hardware watchpoint on the address of interest.
b _start
run
watch *0x20004560 # (for example)
continue (as many times as necessary)
And then looked to see what modified that particular memory location to determine what it was doing. I methodically went through every difference, about 50 of them. I found things like the buffer used to hold CAN-FD frames, timers, nonce counters, the values read by the position sensor and current sensor, and many other things that all seemed perfectly reasonable.
Yet another approach doomed to give no useful information.
Back to an earlier approach
Whatever the problem was, it appeared to be in state on the STM32 that was not accessible to mere mortals. Probably a peripheral got into a bad state that wasn’t exposed via its registers or something. If I couldn’t find the state that was different, could I at least make a “minimal code difference” which was actually minimal?
My C++ minimal difference was pretty small, just the addition of an “=0” to a field initializer. However, that resulted in significant changes in the output program. To make things a little bit more controllable, I tried adding some __asm__("nop") entries to the constructor in question and sure enough, some counts of NOPs would trigger the problem and others wouldn’t. However, they still resulted in large differences in the output.
So then I undertook the painstaking step of gradually turning off optimizations in each function that I saw had changed. In some cases it was as easy as sticking a __attribute__((optimize("O1"))) on the definition. However, in many cases gcc/C++ requires the inline definitions be pulled out-of-line to make that annotation. Both because of that, and just because of bad luck, often these changes would result in my “nop” trick no longer triggering a failure. I worked methodically though, trying new functions until I was eventually able to make a minimal assembly diff that failed.
So far we have learned that simply adding two NOPs to one function that is totally unrelated to the problem in question causes the ADC to become noisy in an odd way. I tried some experimenting to learn more about the failure.
What does adding more NOPs do? The answer… 1 or 2 NOPs fails, 3 or 4 NOPs works, 5 or 6 fails, etc.
Hmmm…. my current top two theories are that either a) it is the instruction layout or b) the execution timing that results in the difference. To rule out one or the other, I made up a series of 8 NOPs, and then substituted a jump in for the first NOP that skipped to one of the later NOPs. That way I could adjust the execution cycle time of the relevant function one by one without changing any layout. That had no effect. Which meant it must have to be the physical layout of the code, not the timing.
The grind
At this point, I undertook what was perhaps the most arduous debugging task yet. To figure out which code was unhappy about having its instruction address changed, I bisected adding NOPs. This wasn’t super straightforward, because as mentioned, gcc’s optimizations generally mean that adding a NOP to a random function results in all kinds of changes all over the place. My procedure was roughly like this:
Identify where in the address space I wanted to add a NOP.
Find a nearby function that was written by me, and not a template expansion or library function.
Switch it to be O1/O0
See if I can still trigger the problem at any of my former test points by adding NOPs (turning off optimizations on the one function sometimes re-ordered everything)
If I can’t, then pick a different function and go back to 1
If I can, then bisect over all my current test points (which may be in a different order than the last bisection) to find the latest address space point where I can add a NOP to trigger the problem
While brutal, I figured this was sure to result in finding the culprit.
And sure enough, after about 15 steps, each taking around 5-10 minutes, it did. I thought the following two lines were the culprit:
The two lines that configure the ADC prescaler! But, wait, didn’t we verify that the ADC prescaler as read from the peripheral registers was the same in both instances? Why yes, we certainly did.
Working:
(gdb) svd/x ADC12_COMMON
Registers in ADC12_Common:
CSR: 0x000A000A ADC Common status register
CCR: 0x000C0001 ADC common control register
CDR: 0x00000000 ADC common regular data register for dual and triple modes
(gdb) svd/x ADC345_COMMON
Registers in ADC345_Common:
CSR: 0x000A000A ADC Common status register
CCR: 0x000C0001 ADC common control register
CDR: 0x05250000 ADC common regular data register for dual and triple modes
Not working:
(gdb) svd/x ADC12_COMMON
Registers in ADC12_Common:
CSR: 0x000A000A ADC Common status register
CCR: 0x000C0001 ADC common control register
CDR: 0x00000000 ADC common regular data register for dual and triple modes
(gdb) svd/x ADC345_COMMON
Registers in ADC345_Common:
CSR: 0x000A000A ADC Common status register
CCR: 0x000C0001 ADC common control register
CDR: 0x05270002 ADC common regular data register for dual and triple modes
For good measure, I tested using stepi to walk through the initialization in the bad state to see if it was somehow related to wall clock timing, but that didn’t make a difference.
Narrowing things down
To avoid the “flavor-of-the-day” the gcc optimizer gives you and make my life easier for experimenting, I rewrote those two lines in inline assembler, just hard-coding the required CCR value:
I added in NOPs before, in between, and after the two stores. To my surprise, in all 3 places failures could be induced, but only on every 4th NOP. Which meant my identification of these two lines was incorrect.
Thus, false alarm. I kept moving down the function, replacing sections with inline assembler and then bisecting with NOPs until I reached the following section:
Here, all 5 ADCs are turned on in rapid succession after previously having all their pre-requisite startup operations and delays performed. NOPs placed before this could cause the ADCs to get into the bad state, but NOPs immediately after did not. Placing NOPs between them always seemed to make the following sections work without problem. Once I had at least 3 NOPs between each, then no amount of change above could cause a failure.
Finally, a decent hypothesis and solution
It seems that the ADCs on the STM32G4 do not like to be turned on in rapid succession, and if they do, bad things can happen like having the prescaler flipped to a different value without it showing in the corresponding register. In this case, the flash accelerator was probably delaying the initialization when the ADEN sets happened such that they crossed a fetch boundary. Then when two of them ended up in the same pre-fetch block, they would get turned on too quickly together. Maybe it causes a local brownout or something? Somewhat recently I upgraded to gcc-11, which probably did a better job of packing these enables into a smaller amount of code space.
I guess that’s an errata for you.
With that understanding, a solution is trivial. Just initialize the ADCs one by one instead of all at once. The initialization sequence for the ADC is documented as requiring a wait until the ADRDY flag is set, so the fix is just to wait for that for each ADC in turn before enabling the next one. For good measure, since initialization isn’t time critical, I switched the whole process to be serial for each ADC, as I expect that is the more tested path with the hardware.
What is the lesson? Hardware is hard? Persistence pays off? I guess you can decide!
As a bonus, now that I know one of the prime symptoms to look for to troubleshoot bad prescalers (unusual bit flips around 2048), I discovered that I could get a bit more performance around the 0 current point by increasing the moteus prescalers a bit (75df013).
It has been on github for a few days now, but I’m excited to announce the newest moteus firmware release, 2022-07-11. This release includes some big features, and some quality of life improvements all around.
Flexible I/O subsystem: This release includes the new flexible I/O subsystem. This adds support for many new encoder types and lets you connect them up in a wide variety of ways.
Cogging torque compensation: Preliminary support for cogging torque compensation is present in this release. It works pretty well on a number of motor types already, future articles will describe it in more detail.
Encoder eccentricity compensation: This feature lets you linearize the output position and velocity in the face of non-linear encoder readings. A write-up for it is also forthcoming in the not-too-distant-future.
Transparent no-BRS CAN-FD communication: If your CAN-FD network is only capable of operating at 1Mbps, and you send queries frames with BRS turned off, moteus will now respond in kind. This eliminates most needs to change the CAN bus frequency due to marginal electrical properties.
This is an exciting time for moteus, and the new features will keep coming!
Third, the official reference documentation can be read here. This describes in detail all of the possible configuration values.
With those niceties out of the way, lets get into the examples!
1: Onboard AS5047P
This configuration is the default for moteus, and uses the onboard absolute AS5047P magnetic encoder for all positioning.
Torque: Yes
Velocity: Good
Position: Good
Configuration: This is the default for moteus, so no configuration is required.
Discussion: This configuration provides a decent all-around compromise between complexity and performance. Torque control is available and velocity control is good outside of ultra-slow regimes. The position is absolutely known to within one rotation of the rotor, across power cycles.
2: Hall effects / hoverboard
Here, the built-in hall effect sensors from a brushless motor are connected to three pins on the ENC port (aux1). Hall effect sensors are sometimes the only sensored commutation method practical to integrate into a motor system, and are the most common means of providing commutation signals for things like hoverboard motors.
Torque: Yes
Velocity: OK
Position: Incremental Only
Hardware: First, for moteus r4.8/11, either a Pico-SPOX 6 connector (TE 5-1775444-6) must be soldered onto the blank ENC pads of a moteus board or a cable must be soldered directly on to the pads. Then a matching cable harness must be constructed to mate the motor’s hall effect sensors to the Pico-SPOX connector (Housing: Molex 0874390601, Terminal: Molex 0874210100). If the sensors require 5V, then a boost regulator and level translators will be required. Otherwise, the 3.3V output of the ENC port can provide up to 100mA of power to the sensors, and their outputs should be connected to the C, K, and O pins (4, 5, and 6 on the connector).
Configuration: This example requires both configuration and alternate options specified duration calibration.
conf set aux1.spi.mode 1 # disabled
conf set aux1.pins.1.mode 6 # hall
conf set aux1.pins.1.pull 1 # pull-up
conf set aux1.pins.2.mode 6 # hall
conf set aux1.pins.2.pull 1 # pull-up
conf set aux1.pins.3.mode 6 # hall
conf set aux1.pins.3.pull 1 # pull-up
conf set aux1.hall.enabled 1 # enabled
conf set motor_position.sources.0.type 4 # hall
To calibrate, two additional options must be passed:
--cal-hall # Instruct moteus_tool to use hall effects
--call-motor-poles 30 # How many motor pole pairs are present
Discussion: Performance-wise, this is a relatively poor option. Torque control is available, which is mostly what is used in hoverboard applications. Since the effective encoder resolution is very coarse, velocity control only works well at moderate to high speeds (say 0.3 Hz or higher). Position control is *not* absolutely referenced (i.e. it starts from 0 every time you power on) and it is only good to around 4 degrees for 30 pole pair hoverboard motors. With the current moteus configuration options, there is often jitter when stopped when using velocity or position control.
3: External AS5047P
This configuration can be used when the performance of the onboard AS5047P would be adequate, but for whatever reason, the moteus controller cannot be positioned immediately behind the sense magnet.
Torque: Yes
Velocity: Good
Position: Good
Hardware: Like with the hall effect example, for moteus r4.8/11, this requires that a Pico-SPOX connector be populated on the ENC blank pads (TE 5-1775444-6) or that a cable be directly soldered to the pads. A cable must then be constructed to attach the external AS5047P to the mating Pico-SPOX connector (Housing: Molex 0874390601, Terminal: Molex 0874210100). All of the pins are required:
G: Ground
3: 3.3V
C: SPI chip select
K: SPI clock
I: SPI CIPO/MISO
O: SPI COPI/MOSI
As the SPI lines are high frequency, they must be relatively short (<20cm), and mounted in such a way to avoid electrical interference. That may require shielding around the cable, or around portions of the mechanical assembly.
Configuration: The following configuration changes are necessary from a default setup:
conf set aux1.pins.0.mode 2 # spi_cs
conf set aux1.pins.1.mode 1 # spi
conf set aux1.pins.2.mode 1 # spi
conf set aux1.pins.3.mode 1 # spi
conf set aux1.spi.mode 2 # ext_as5047
Discussion: After the hardware has been constructed and the above configuration changes made, use and performance is identical to the onboard AS5047P configuration.
4: Sine / Cosine
Some absolute encoders emit their output in the form of two analog signals, the “sine” and “cosine”. For a given angle, each signal emits an analog voltage biased about a non-zero operating point based on its namespace. If the sine and cosine are 3.3V level, or can be downshifted using a resistive divider to fit within that range, then they can be used with moteus.
Hardware: Like with the hall effect example, for moteus r4.8/11, this requires that a Pico-SPOX connector be populated on the ENC blank pads (TE 5-1775444-6) or that a cable be directly soldered to the pads. A cable must then be constructed to attach the encoder to the mating Pico-SPOX connector (Housing: Molex 0874390601, Terminal: Molex 0874210100). The sine and cosine channels must be connected to one of:
ENC pin 4 / K / aux1 pin 1
ENC pin 5 / I / aux1 pin 2
ENC pin 6 / O / aux1 pin 3
Any two of those may be used. The ground signal should be connected, and the 3.3V signal can be used to provide up to 100mA of power to the target encoder.
Configuration: The following configuration changes are required from a default setup:
conf set aux1.pins.1.mode 8 # sine
conf set aux1.pins.2.mode 9 # cosine
conf set aux1.spi.mode 1 # disabled
conf set aux1.sine_cosine.enabled 1 # enabled
The next value must be calibrated. The raw values of the sine and cosine channel can be observed in telemetry as the encoder rotates through several revolutions. Take the midpoint, and use it below:
conf set aux1.sine_cosine.common 1692 # from hand-calibration
Calibration requires no special options.
Discussion: As a sine/cosine encoder is just an alternate way of connecting a rotor absolute position sensor, the basic properties are the same as for the onboard AS5047P encoder. However, the analog signal has less effective resolution, and more noise than a digital encoder. Thus, the audible noise may be greater for a given selected encoder bandwidth, and the control performance may be worse.
5: Quadrature / Index (ABI)
A lowest common denominator of absolute encoders is the quadrature and index interface. Here, two digital signal lines step through a quadrature relationship as ticks on the encoder pass, and a separate “index” line becomes positive at the 0 point of the encoder. This does provide absolute positioning, but requires that the motor possibly spin through an entire rotation at power on in order to discover what that absolute position is.
Torque: Yes
Velocity: Good
Position: OK
Hardware: Like with the hall effect example, for moteus r4.8/11, this requires that a Pico-SPOX connector be populated on the ENC blank pads (TE 5-1775444-6) or that a cable be directly soldered to the pads. A cable must then be constructed to attach the encoder to the mating Pico-SPOX connector (Housing: Molex 0874390601, Terminal: Molex 0874210100). The three signals can be connected to any of the following pins:
ENC pin 2 / C / aux1 pin 0
ENC pin 4 / K / aux1 pin 1
ENC pin 5 / I / aux1 pin 2
ENC pin 6 / O / aux1 pin 3
The ground signal should be connected, and the 3.3V pin may be used to provide up to 100mA of power to the target encoder.
Configuration: The following must be changed from default (assuming that the I pin is connected to K/ aux1 pin 1, and the two quadrature lines are connected to I / aux1 pin 2, and O / aux1 pin 3.
conf set aux1.pins.1.mode 7 # index
conf set aux1.pins.2.mode 4 # quad_sw
conf set aux1.pins.3.mode 4 # quad_sw
conf set aux1.spi.mode 1 # disabled
conf set aux1.quadrature.enabled 1 # enabled
Quadrature encoders have a rated cycles per revolution or counts per revolution. The counts per revolution is 4x the cycles per revolution, and is what moteus requires. The following configuration is suitable for a 1000 cycle per revolution or 4000 counts per revolution encoder.
conf set aux1.quadrature.cpr 20000
conf set motor_position.sources.0.type 3 # quadrature
conf set motor_position.sources.0.cpr 4000
Calibration requires no special options.
Discussion: This option can provide nominally the same performance as the onboard encoder with two caveats.
First, a “homing” step must be performed before control can begin. This means that actuating the motor must be possible without damage from the turn-on state, and not incompatible with the end application.
Second, it is possible for counts to be “missed” because of electrical interference. This can result in decreased performance or loss of control depending upon the severity. Proper routing and shielding of quadrature lines are essential to robust performance. The aux1.quadrature.error diagnostic field can be used to monitor for events that are consistent with electrical interference (it is normal to start at 1 at turn-on).
6: I2C Disambiguator
Often a motor controlled by moteus drives a gear reducer before eventually driving the output. In such cases, the onboard moteus encoder is not able to uniquely determine which sector the reducer output is located. One means of resolving that ambiguity is to place a low-rate absolute I2C based encoder on the output shaft.
Hardware: The only pins that support I2C on moteus r4.3/5/8/11 are the two data pins on the ABS port (a JST ZH-4 connector). A harness must be constructed to connect those two signal lines. On an unmodified board, 3.3V resistive pullups are hard-wired onto the board. If a 5V sensor is desired, either a boost regulator will be required to operate it from the ABS port power, or a separate 5V regulator will be necessary to power it.
ABS pin 1 – 3.3V
ABS pin 2 / aux2 pin 0 – SCL
ABS pin 3 / aux2 pin 1 – SDA
ABS pin 4 – GND
Configuration: Assuming an AS5048B encoder is attached, the following configuration changes are required:
conf set aux2.pins.0.mode 13 # i2c
conf set aux2.pins.1.mode 13 # i2c
conf set aux2.spi.mode 1 # disabled
conf set aux2.i2c.devices.0.type 1 # as5048
conf set motor_position.sources.1.aux_number 2
conf set motor_position.sources.1.type.i2c 7 # i2c
conf set motor_position.sources.1.i2c_device 0
conf set motor_position.sources.1.cpr 65536 # for the as5048
conf set motor_position.sources.1.reference 1 # output
Since two encoders must be correlated, the sign and offsets should be manually calibrated. Set them such that motor_position.sources.0.compensated_value in the diagnostics pane increases in the same direction for both and that it is 0 for both at the same position. Additionally, the gear box reduction ration needs to be set.
conf set motor_position.sources.1.sign 1 # hand-calibrated
conf set motor_position.sources.1.offset 0 # hand-calibrated
conf set motor_position.sources.0.offset 0 # hand-calibrated
conf set motor_position.rotor_to_output_ratio 0.5 # reduction ratio
Discussion: The control performance of this example is identical to that of the primary encoder in use, which here would be the onboard AS5047P. The advantage is that the absolute position of the output shaft is known exactly at power on, even with the reducing stage.
7: Onboard + Quadrature Output
For this example, a high resolution quadrature encoder is used in combination with the onboard encoder to improve the velocity and position tracking performance of the controller.
Torque: Yes
Velocity: Excellent
Position: Excellent
Hardware: Since the onboard encoder is being used, the quadrature encoder must be connected to the 2 pins on the ABS port (aux2) using a JST ZH-4 connector.
ABS pin 1 – 3.3V
ABS pin 2 / aux2 pin 0 – Quadrature A
ABS pin 3 / aux2 pin 1 – Quadrature B
ABS pin 4 – GND
Configuration: This configuration assumes that a 5000 cycle per revolution / 20000 count per revolution encoder is being used. For that, the following changes must be made:
conf set aux2.spi.mode 1 # disabled
conf set aux2.pins.0.mode 4 # quad_sw
conf set aux2.pins.1.mode 4 # quad_sw
conf set aux2.quadrature.enabled 1 # enabled
conf set aux2.quadrature.cpr 20000
conf set motor_position.sources.1.aux_number 2
conf set motor_position.sources.1.quadrature 3 # quadrature
conf set motor_position.sources.1.cpr 20000
Since multiple encoders are being correlated, we need to manually inspect the motor_position.sources telemetry view to identify if both sources increase as the output is moved in the same direction. If not, then the sign of the quadrature source should be -1.
conf set motor_position.sources.1.sign -1
Finally, the output position is configured to be derived from the quadrature signal, with a reference source from the onboard encoder so that it starts at the correct absolution position.
conf set motor_position.output.source 1
conf set motor_position.reference_source 0
Discussion: For this example, a quadrature encoder of much higher resolution than the onboard encoder was selected. Thus the resulting control performance has improved control at very low speeds, and improved position stiffness at any speed. The relative performance gain is directly proportional to the increase in effective resolution.
8: RLS AksIM-2
The RLS AksIM-2 is an encoder that reads a ring with magnetic tracks encoded on it to produce high resolution absolute position. It can be used when a hollow shaft is desired, or when high performance velocity control or high stiffness position control is required.
Torque: Yes
Velocity: Excellent
Position: Excellent
Hardware: moteus only supports the RS-422 asynchronous serial configuration of the RLS AksIM-2. The exact part numbers used in this demonstration are:
MB049SFF17BDNT00 – encoder
MRA049BG034DSN00 – magnetic ring
ACC049 – development cable
It requires the two data pins on the ABS port of moteus r4.3/5/8/11. To use it, a RS-422 level transceiver is required. One option which has been demonstrated to work is the MAX3490. Additionally, the AksIM-2 requires 5V power, this can be provided via a boost regulator driven from the ABS port or a separate 5V regulator.
ABS pin 1 – 3.3V
ABS pin 2 / aux2 pin 0 – RX
ABS pin 3 / aux2 pin 1 – TX
ABS pin 4 – GND
Configuration: The required configuration is straightforward. This example will leave the onboard encoder configured, but it could also be disabled if not used. It also assumes that the AksIM-2 has been configured ahead of time (either at the factory or using RLS tools) to 1000000 baud.
conf set aux2.pins.0.mode 3 # uart
conf set aux2.pins.1.mode 3 # uart
conf set aux2.spi.mode 1 # disabled
conf set aux2.uart.mode 1 # aksim2
conf set aux2.uart.baud_rate 1000000
conf set motor_position.sources.1.aux_number 2
conf set motor_position.sources.1.type 2 # uart
conf set motor_position.sources.1.cpr 4194304 # 22 bits (for any AskIM-2)
conf set motor_position.commutation_source 1
conf set motor_position.output.source 1
Discussion: This example provides excellent control performance both at very low speeds, high position stiffness, and gives a hollow shaft capability. The primary downsides are complexity and cost. An active adapter is required, and in single unit quantities, the AksIM-2 and an encoder ring is more than twice as expensive than a moteus itself.
9: iC-Haus iC-PZ
The iC-Haus iC-PZ is a reflective optical encoder that reads a marked code ring. It is high resolution and accuracy and supports a number of communication interfaces. The only one that moteus supports currently is the SPI protocol.
Torque: Yes
Velocity: Excellent
Position: Excellent
Hardware: Like with the hall effect example, for moteus r4.8/11, this requires that a Pico-SPOX connector be populated on the ENC blank pads (TE 5-1775444-6) or that a cable be directly soldered to the pads. A cable must then be constructed to attach the external AS5047P to the mating Pico-SPOX connector (Housing: Molex 0874390601, Terminal: Molex 0874210100).
Additionally, while the iC-PZ can provide 3.3V compatible I/O pins, and thus no SPI level translation is required, it also requires a 5V supply. Thus either a boost regulator is required to use the ENC port provided 3.3V power, or a separate 5V supply is needed.
All of the pins are required:
G: Ground
3: 3.3V
C: SPI chip select
K: SPI clock
I: SPI CIPO/MISO
O: SPI COPI/MOSI
Configuration: The iC-PZ requires a moderate amount of provisioning before it will produce usable values. While it is possible to do so using only diagnostic mode commands with moteus, the process is involved enough that it is not recommended. Instead, this configuration assumes that the encoder has been provisioned using the iC-Haus tools first.
conf set aux1.pins.0.mode 2 # spi_cs
conf set aux1.pins.1.mode 1 # spi
conf set aux1.pins.2.mode 1 # spi
conf set aux1.pins.3.mode 1 # spi
conf set aux1.spi.rate_hz 6000000
conf set aux1.spi.mode 3 # ic_pz
conf set motor_position.sources.0.cpr 16777216 # 24 bits
Discussion: The iC-PZ provides the same benefits as the RLS AksIM-2 above in terms of improved control performance and hollow shaft capability. It still requires adapter hardware with moteus, although the required hardware is slightly less complex than with the AksIM-2. Also, it is lower cost, but does require a more complex provisioning stage and optical cleanliness.
10: Fixed Voltage
Sometimes you can’t fit an encoder into a design, but you still want low speed operation. For those cases, moteus can be configured to drive a brushless motor as if it were a stepper motor with micro-steps. This means that there is no velocity or position feedback, and the motor will constantly dissipate a roughly fixed power, even when stationary or unloaded.
Torque: No
Velocity: Poor
Position: Poor / Incremental Only
Hardware: No additional hardware is required.
Configuration: Auto-calibration is not possible in this mode, but the required configuration is minimal. The number of pole-pairs must be entered manually if the output is to be scaled correctly, and the fixed control voltage must be specified by the designer. A larger fixed voltage will dissipate more power but provide more holding torque.
conf set motor.poles 14 # the number of pole-pairs for the motor
conf servo.fixed_voltage_mode 1 # enabled
conf servo.fixed_voltage_control_V 0.3 # selected by the designer
Discussion: In most cases where you want to drive a motor in a stepper like manner, a dedicated stepper driver is more appropriate. However, if you already have multiple moteus controllers in your system, or you want to use the advanced trajectory controls, this might be a valid design choice.
11: GPIO
Any of the pins on any connector can be used for 3.3V digital input or output. Some pins can be used for 3.3V analog input, and some can be used for 5V digital input. A dedicated “function” on the aux port is not required, so any pins which are not otherwise needed for a function can be so configured.
Hardware: moteus does not require any specific hardware, although if you want to use a given digital input or output to do something, you will likely need to construct at a minimum a harness to connect it to one of the ENC or ABS ports.
Configuration: Each pin can be configured independently:
conf set aux2.pins.0.mode 14 # digital input
conf set aux2.pins.1.mode 15 # digital output
conf set aux1.pins.2.mode 16 # analog input
Discussion: Once configured, the digital readings from each pin are accessible in the auxN.pins diagnostic mode tree and by using the 0x05e or 0x05f register mode registers as 7 bit integer bitfields.
Writing to the digital outputs can be conducted with the diagnostic protocol as follows
aux1 out 33 # a decimal bitfield
or by using register mode registers 0x05c / 0x05d.
Finally, analog inputs can be read in the diagnostic tree at aux1.analog_inputs, and in register mode at registers 0x060 – 0x06c.
Conclusion
While the flexible I/O subsystem already enables a lot of new configurations for moteus, keep on the lookout as it makes even more things feasible in the future!
This will be the final post describing the fundamentals of configuring the new flexible I/O system. There have been a number of previous posts (part 1, part 2, part 3). In this iteration, we’ll cover how to configure the sinks that consume the “source” encoder data. As a reminder, the block diagram of the I/O system looks like:
Commutation
To perform commutation with field oriented control, moteus needs to know the relationship between the rotor and stator in the magnetic domain. With the addition of the new flexible I/O system, some of the configurable values associated with this remain as they were, where there are some new ones.
First, the number of poles for the motor is still at motor.poles, and whether or not to invert the ordering of the output phases is at motor.phase_invert. Similarly, the theta mapping table has the same semantics before and remains at motor.offset.
Newly added is motor_position.commutation_source which controls which 0 indexed source is used to drive commutation.
It is shown in the block diagram above, but not discussed here yet are the cogging compensation parameters. They’ll be covered soon, I promise!
Output
The other primary purpose of encoder data within moteus is to act as feedback to the position control loop. For this, the source to use can be selected with:
motor.output.source
Additionally, an offset and sign can be configured at this stage with motor.output.offset or motor.output.sign. A gear reducer can be configured by entering a non-unity value for
motor.rotor_to_output_ratio
Values here will typically be smaller than 1. For instance, a 1:4 gear reducer would be 0.25.
Finally, to support disambiguation after a reducer with low-performance sensors, a second source may be configured for that purpose with:
motor.output.reference_source
The “reference source” will be consulted only at system power on after it is valid, in order to resolve the ambiguity resulting from the reducer. It can also be used if the output is incremental, but another absolute source is present that is lower quality or lower rate, to reference the output at power on.
Application
There are no configurable values necessary to operate any of the new I/O system from the application level. There are new registers that can be used to read the position and velocity from each of the sources, whether or not they are configured for any sinks. Similarly, the digital input, digital output, and analog inputs can all be operated from the register or diagnostic protocol. All of these can be seen in the reference documentation.
In the last two posts (part 1, part 2), I started talking about the new, more flexible I/O subsystem for the open source moteus brushless motor controller. In this post, I’ll continue by describing what a “source” is, and how it is configured.
For reference, the block diagram showing how auxiliary ports, sources, and sinks are related is below:
Each “source” in the above diagram represents a single encoder. To the sinks it provides a position and velocity, along with various validity indications for that data. Each has three basic configuration components: where to get the raw data, how to transform that raw data, and the low-pass filter configuration. We’ll cover each in turn.
Source raw data location
The location where a source retrieves raw data is selected by picking the auxiliary port number to use (1 indexed, so 1 is auxiliary port 1) and which function within that auxiliary port the data should be retrieved from. For instance, if we want a quadrature encoder connected to auxiliary port 2 to feed into source 3, we would set:
There are two other minor complications. The first, is that for I2C functions, a separate configurable value is used to select which I2C device should be used.
motor_position.sources.X.i2c_index
If an “incremental” raw source is used, like quadrature, then an index function can be used directly at the source level to allow the source itself to report a correct absolute reading. This is necessary if the source is to be used for commutation. It is configured by entering the 1 based auxiliary port number into:
motor_position.sources.X.incremental_index
Source transforms
The are a number of transforms applied in sequence, each of which may be configured:
Offset: This is an offset in count space to add to the raw value before any other transformation is performed.
Sign: This may be 1 or -1. If -1, then the value will be inverted.
CPR: This is the “counts per revolution” and must be configured. For auxiliary port functions which have a CPR configuration, this must match it.
Eccentricity Compensation: A table of 32 points that describe percentage offsets from a perfectly linear response. It can be generated through a separate calibration tool, and all 0’s performs no eccentricity compensation.
The nominal output after all transforms have been completed is an angular position value between 0.0 and 1.0.
Low pass filter
The PLL based low pass filter is the final stage in the source pipeline. It is configured through a single parameter to select the 3dB cutoff frequency:
motor_position.sources.X.pll_filter_hz
If the encoder is intended to be used for either commutation or for output position control, this frequency should be at least as large as the mechanical bandwidth of the system and at least as large as the torque bandwidth for stable control. Higher values than that trade off audible noise versus control performance.
Reference frame
Each source can be configured to be in one of the “rotor” or the “output” reference frames. This controls whether the rotor to output ratio is applied when using the value for either commutation or output control.
motor_position.sources.X.reference
Where 0 is used for rotor and 1 is used for output.
Nearly done with configuration
I’ll tackle the final part of configuration in the next post, the sinks that use this encoder data.