Close

featuring v0.7

A project log for custom bldc motor board using fpga

custom pcb for controlling a bldc motor optimally

Simon TrendelSimon Trendel 02/26/2020 at 20:530 Comments

The following image shows an overview of the components used in v0.7:

The following section gives some details about the motor control code.

We are using the Texas Instrument drv8323hrta chip in independent mode. This means we have full control over the mosfet gates. Commuting the phases seemed trivial at first, but as it turns out, you need to know the specs of your mosfets really well in order to do this right. The key word here is dead time insertion. It essentially refers to the mosfets not being ideal switches and not turning on or off instantaneously, but with a certain delay. When we commute the phases, we have to be sure the opposing mosfets are closed, otherwise you can easily burn your mosfets by shoot through gate states. One common way is to use dead time insertion. The exact timing can be calculated from the specs of our mosfets, and I will make a mental note now to do that later. For now we are just using an arbitrary large enough dead time. For our application the motor spins quite slow and the big dead time will be really only noticeable at high motor speeds. Here is the code that handles our bldc commutation:

always @(posedge clk16MHz) begin: BLDC_COMMUTATION
   commutation_state_prev <= commutation_state;

   if(commutation_state!=commutation_state_prev)begin
     dti <= 1;
     dti_counter <= 1024;
   end

   if(hall1 && ~hall2 && hall3)begin
     commutation_state <= A;
   end
   if(hall1 && ~hall2 && ~hall3)begin
     commutation_state <= B;
   end
   if(hall1 && hall2 && ~hall3)begin
     commutation_state <= C;
   end
   if(~hall1 && hall2 && ~hall3)begin
     commutation_state <= D;
   end
   if(~hall1 && hall2 && hall3)begin
     commutation_state <= E;
   end
   if(~hall1 && ~hall2 && hall3)begin
     commutation_state <= F;
   end

   if(dti) begin
     if(dti_counter==0)begin
       dti <= 0;
     end else begin
       dti_counter <= dti_counter -1;
       GHA <= 0; GLA <= 0; GHB <= 0; GLB <= 0; GHC <= 0; GLC <= 0;
     end
   end else begin
    if(dir)begin
      case(commutation_state)
         B: begin
           GHA <= 1; GLA <= 0; GHB <= 0; GLB <= 1; GHC <= 0; GLC <= 0;
         end
         C: begin
           GHA <= 1; GLA <= 0; GHB <= 0; GLB <= 0; GHC <= 0; GLC <= 1;
         end
         D: begin
           GHA <= 0; GLA <= 0; GHB <= 1; GLB <= 0; GHC <= 0; GLC <= 1;
         end
         E: begin
           GHA <= 0; GLA <= 1; GHB <= 1; GLB <= 0; GHC <= 0; GLC <= 0;
         end
         F: begin
           GHA <= 0; GLA <= 1; GHB <= 0; GLB <= 0; GHC <= 1; GLC <= 0;
         end
         A: begin
           GHA <= 0; GLA <= 0; GHB <= 0; GLB <= 1; GHC <= 1; GLC <= 0;
         end
       endcase
     end else begin
      case(commutation_state)
        B: begin
          GHA <= 0; GLA <= 1; GHB <= 1; GLB <= 0; GHC <= 0; GLC <= 0;
        end
        C: begin
          GHA <= 0; GLA <= 1; GHB <= 0; GLB <= 0; GHC <= 1; GLC <= 0;
        end
        D: begin
          GHA <= 0; GLA <= 0; GHB <= 0; GLB <= 1; GHC <= 1; GLC <= 0;
        end
        E: begin
          GHA <= 1; GLA <= 0; GHB <= 0; GLB <= 1; GHC <= 0; GLC <= 0;
        end
        F: begin
          GHA <= 1; GLA <= 0; GHB <= 0; GLB <= 0; GHC <= 0; GLC <= 1;
        end
        A: begin
          GHA <= 0; GLA <= 0; GHB <= 1; GLB <= 0; GHC <= 0; GLC <= 1;
        end
      endcase
     end
   end
 end

The hall sensor inputs control the commutation state we are in. Depending on the direction we want to turn the motor, the respective gates are opened. Whenever the commutation state changes, dead time is inserted (dti).

We are using a 20kHz PWM signal for the gates. The PWM is calculated in the following way:

localparam CLK_FREQ   = 16_000_000;
localparam PWM_FREQ   = 20_000;

reg signed [23:0] pwm_counter;

always @(posedge clk, posedge reset) begin: PWM
  if(reset) begin
    pwm_counter <= 0;
  end else begin
    pwm_counter <= pwm_counter+1;
    if(pwm_counter>=(CLK_FREQ/PWM_FREQ)) begin
      pwm_counter <= 0;
    end
    if(pwm_counter<duty)begin
      pwm_out <= 1;
    end else begin
      pwm_out <= 0;
    end
  end
end

This gives us 16_000_000/20_000 = 800 range for our PI controller: 

localparam  CLOCK_FREQ = 16_000_000;
localparam  CONTROL_FREQ = 1000;

reg signed [23:0] result;
assign duty = result;
reg control_update;
integer counter;

always @ ( posedge CLK ) begin
  counter <= counter+1;
  control_update <= 0;
  if(counter>(CLOCK_FREQ/CONTROL_FREQ))begin
    counter <= 0;
    control_update <= 1;
  end
end

always @ ( posedge CLK , posedge reset) begin: PID_CONTROLLER
  reg signed [23:0] err;
  reg signed [23:0] err_prev;
  reg signed [23:0] integral;
  if(reset)begin
    err = 0;
    err_prev <= 0;
    result = 0;
    integral = 0;
  end else begin
    if(control_update)begin
      err = (setpoint-state);
      integral = integral+err;
      if(integral>IntegralLimit) begin
        integral = IntegralLimit;
      end else if(integral<-IntegralLimit) begin
        integral = -IntegralLimit;
      end
      result = Kp*err + Ki*integral;
      if((result>deadband) || (result < -deadband))begin
        if(result>PWMLimit)begin
          result = PWMLimit;
        end else if(result<-PWMLimit)begin
          result = -PWMLimit;
        end
      end else begin
        result = 0;
      end
    end
  end
end

 We chose the control frequency to be 1000, which seems reasonable for our motor.

Next lets talk about communication. We augmented the icebus protocol with a few more fields and modified the automatic setpoint and control mode update cycles on the de10 side. We reduced the size of all 32-bit signed integers to 24-bit signed, which reduces the LEs consumption a lot. This was necessary because we started to hit the limit on whats available on our precious iCE40LP8k.



The message types are summarized here:

           header 0xD0D0D0D0 (4 byte)

            motorID (1 byte)

           setpoint (3 byte)

           neopxl_color (3 byte)

           crc16 (2 byte)

           header 0xBAADAA55 (4 byte)

           motorID (1 byte)

           control mode (1 byte)

           Kp (2 byte)

           Ki (2 byte)

           Kd (2 byte)

           PWMLimit (3 byte)

           IntegralLimit (3 byte)

           deadband (3 byte)

           setpoint (3 byte)

           current_limit (2 byte)

           crc16 (2 byte)

           header 0x1CE1CEBB (4 byte)

           motorID (1 byte)

           crc16 (2 byte)

           header 0x1CEB00DA (4 byte)

           motorID (1 byte)

           encoder0_position (3 byte)

           encoder1_position (3 byte)

           setpoint (3 byte)

           duty (3 byte)

           displacment (3 byte)

           current (2 byte)

           neopxl_color (3 byte)

           crc16 (2 byte)

The icebus protocol was augmented by a couple of fields. The following image shows a logic analyser capture of the status_request/response mechanism:

The host initiates a status request with the corresponding header 0x1CE1CEBB followed by the motor_id (in this case 0x80) and the crc16 checksum. The motorboard with this motor_id then replies with the status response message. In case either the control_mode or setpoint does not match, an automatic command message is send by the host:

The baud rate in the example above was set to 2MHz, which allows control frequencies of 500 Hz for 10 motors on a icebus.

If you want to talk to multiple iceboards, each one needs a unique 8bit ID. This ID can be conveniently stored on the 16kB eeprom on the iceboard. The ID is read when the board boots via i2c. The eeprom can be written with an arduino board using this sketch. Before, the ID was hard-coded into each fpga build. The arduino sketch also allows you to write the desired baudrate into the eeprom. In the future the eeprom can be used to store iceboard specific stuff, like eg parameters of a calibration of one of our muscle units.

Last but not least, we implemented a spi module to read out our TLI4970 current sensor. The current can now be limited too, for example to 1 amp and the iceboard will automatically throttle the pwm if this value is exceeded. This can be extremely useful if your motor only supports a certain continuous current, and or you cannot cool your motor enough under certain loads. In any case this lets me sleep better because burning one of our maxon motors really hurts.

Now before I go, I should mention the fuck-ups on this version. There are three:

  1. wrong components in the BOM. The 3.3V DC-DC converter, the DIP switch and two or three caps had the wrong footprint.
  2. chip enable pin of the 5V->3.3V logic level chip was pulled low instead of high. Fortunately the 3.3V clock supply pin was very close. If you zoom in on one of the boards you can see the 0402 pull-up connected to the clk pin.
  3. In the schematics a few of the resistors for the two encoders were
    marked as optional for impedance matching, but they made it into the BOM and got assembled. We actually did not check if those would be fine, but the 10k pull-ups on those lines were definitely too weak and had to be replaced manually by 1k

In the next log we will show the results of excessive endurance testing of our v0.7 boards.

Discussions