The software is possibly the most complex part of the project to develop.
Here strong belief is in simplicity, small steps and peer reviewing.
Because of this, the software will be open source and the steps well documented for maximum peer understanding.
However this is a fixed 50/50 mark/space.
This is good as a starting point, but it is necessary to vary the mark/space.
This is where things go wrong, since every instruction take time.
; Intialise the mark/space times
MOVLW 63 ; 1 <= count <= 63
MOVWF SPACELENGTH ; Set the space time
MOVLW 1 ; 1 <= count <= 63
MOVWF MARKLENGTH ; Set the mark time
GOTO PCM_LOOP
; *********************************************
; * PCM Routine *
; *********************************************
PCM_LOOP MOVLW B'00000000' ; Set all LEDS off
MOVWF PORTB ; Send to PORT B
MOVF SPACELENGTH,W ; Get the space time
MOVWF SPACETIMER ; Initialise the timer
SPACE_LOOP DECFSZ SPACETIMER,F ; Count down
GOTO SPACE_LOOP ; Loop until zero
NOP ; Give symmetry
NOP ; Give symmetry
MOVLW B'00111111' ; Set all LEDS on
MOVWF PORTB ; Send to PORT B
MOVF MARKLENGTH,W ; Get the mark time
MOVWF MARKTIMER ; Initialise the timer
MARK_LOOP DECFSZ MARKTIMER,F ; Count down
GOTO MARK_LOOP ; Loop until zero
GOTO PCM_LOOP
So a mechanism is needed for varying the mark/space ratio.
There is another way to to this:
A fixed loop which counts 0 to 255 can be used and a variable which holds the mark time.
The loop scans with bits high until the magic value then flips to low.
This is similar to a frame relay used in mobile phones to transmit and receive data to/from the base radio.
This is probably more robust than timers since the the loop always executes the same instructions and may loop faster.
Also the ability to have two(or more) registers for 16-bit (or higher) resolution.
At this stage it might be worth pointing out that it's obviously unrealistic to expect the final controller to be using a PIC.
Most likely it will be using an EPIA motherboard or similar and running a real-time Linux kernel (livecd).
This will make communication easy via ethernet and the ability to develop the real-time controller software using a frame relay built as a RT process in the pre-emptive scheduler.
Also the PIC16F628 which is being tested has built-in PWM outputs, so it would seem a better course of action to use these.
This is an animation of the fields and phases of 3-phase DC motor commutation.
This is actually the same whether brushed or brushless.
In a brushed motor the commutation is mechanical using a commutator attached to the shaft of the armature and usually carbon brushes.
The animation represents the fields on the armature with respect to the shaft as it rotates.
The actual field with respect to the stator magnets (or the case) is more or less in the same direction at approximately 90 degrees to the stator field for maximum torque.
In a brushless motor the magnets are on the rotor and the stator field is rotated to draw the rotor field around.
This rotating stator field is provided by 3 electromagnets (U, V and W in the animation) at 120 degrees to each other and energised in sequence using a controller.
This is the commutation aimed for in the PIC software, which is easy to translate using the table on the left of the animation.
; ***********************************************
; * PCM Routine which does BLDC commutation *
; * Without the delays rotates in 48uS (21kHz) *
; ***********************************************
PCM_LOOP MOVLW B'00011010' ; 1
MOVWF PORTB
CALL DELAY_ROUTINE
NOP ; Give symmetry
NOP ; Give symmetry
MOVLW B'00010010' ; 2
MOVWF PORTB
CALL DELAY_ROUTINE
NOP ; Give symmetry
NOP ; Give symmetry MOVLW B'00010110' ; 3
MOVWF PORTB
CALL DELAY_ROUTINE
NOP ; Give symmetry
NOP ; Give symmetry
MOVLW B'00000110' ; 4
MOVWF PORTB
CALL DELAY_ROUTINE
NOP ; Give symmetry
NOP ; Give symmetry
MOVLW B'00100110' ; 5
MOVWF PORTB
CALL DELAY_ROUTINE
NOP ; Give symmetry
NOP ; Give symmetry
MOVLW B'00100100' ; 6
MOVWF PORTB
CALL DELAY_ROUTINE
NOP ; Give symmetry
NOP ; Give symmetry
MOVLW B'00100101' ; 7
MOVWF PORTB
CALL DELAY_ROUTINE
NOP ; Give symmetry
NOP ; Give symmetry
MOVLW B'00100001' ; 8
MOVWF PORTB
CALL DELAY_ROUTINE
NOP ; Give symmetry
NOP ; Give symmetry
MOVLW B'00101001' ; 9
MOVWF PORTB
CALL DELAY_ROUTINE
NOP ; Give symmetry
NOP ; Give symmetry
MOVLW B'00001001' ; 10
MOVWF PORTB
CALL DELAY_ROUTINE
NOP ; Give symmetry
NOP ; Give symmetry
MOVLW B'00011001' ; 11
MOVWF PORTB
CALL DELAY_ROUTINE
NOP ; Give symmetry
NOP ; Give symmetry
MOVLW B'00011000' ; 12
MOVWF PORTB
CALL DELAY_ROUTINE
GOTO PCM_LOOP
DELAY_ROUTINE MOVLW H'7F'
MOVWF TIMER1
DEL_LOOP1 MOVLW H'FF'
MOVWF TIMER2
DEL_LOOP2 DECFSZ TIMER2,F
GOTO DEL_LOOP2
DECFSZ TIMER1,F
GOTO DEL_LOOP1
RETLW 0
; + + +
; UUVVWW
; - - -
STEP01 EQU B'00011010'
STEP02 EQU B'00010010'
STEP03 EQU B'00010110'
STEP04 EQU B'00000110'
STEP05 EQU B'00100110'
STEP06 EQU B'00100100'
STEP07 EQU B'00100101'
STEP08 EQU B'00100001'
STEP09 EQU B'00101001'
STEP10 EQU B'00001001'
STEP11 EQU B'00011001'
STEP12 EQU B'00011000'
BLDC_LOOP MOVLW STEP01 ; Get bit pattern for BLDC step
MOVWF PORTB ; Set output lines
CALL DELAY_ROUTINE ; Do intersample delay
NOP ; Give symmetry
NOP ; Give symmetry
. . . steps 02 - 11 removed for clarity
MOVLW STEP12 ; Get bit pattern for BLDC step
MOVWF PORTB ; Set output lines
CALL DELAY_ROUTINE ; Do intersample delay
GOTO BLDC_LOOP ; Repeat BLDC sequence (2 cycles)
DELAY_ROUTINE MOVF TIME1,W ; Get initial count
MOVWF TIMER1 ; Set the timer
DEL_LOOP DECFSZ TIMER1,F ; Count down
GOTO DEL_LOOP ; Repeat
MOVF TIME3,W ; Get initial count
MOVWF TIMER3 ; Set the timer
DEL_LOOP4 DECFSZ TIMER3,F ; Count down
GOTO DEL_LOOP4 ; Repeat
INCFSZ TIME2,1 ; Count up button scan timer (256 wrap around)
GOTO ENDLOOP ; Jump out if non-zero
GOTO UPDOWN ; Jump to button scan if zero (every 256 delays)
ENDLOOP NOP ; Give symmetry
NOP ; Give symmetry
NOP ; Give symmetry
NOP ; Give symmetry
NOP ; Give symmetry
RETLW 0 ; Return to BLDC loop
UPDOWN BTFSC PORTA,SW1 ; Skip next if SW1 is up
DECFSZ TIME1,F ; Decrement first timer initial value
GOTO UP ; Jump if first timer initial value is non-zero
INCF TIME1,F ; Re-increment first timer if zero
UP BTFSC PORTA,SW2 ; Skip next if SW2 is up
INCFSZ TIME1,F ; Increment first timer initial value
GOTO UPDOWN3 ; Jump if first timer initial value is non-zero
DECF TIME1,F ; Re-decrement to 255 first timer if zero
UPDOWN3 BTFSC PORTA,SW3 ; Skip next if SW3 is up
DECFSZ TIME3,F ; Decrement second timer initial value
GOTO UP3 ; Jump if second timer initial value is non-zero
INCF TIME3,F ; Re-increment second timer if zero
UP3 BTFSC PORTA,SW4 ; Skip next if SW4 is up
INCFSZ TIME3,F ; Increment second timer initial value
GOTO BACK ; Jump if second timer initial value is non-zero
DECF TIME3,F ; Re-decrement to 255 first timer if zero
BACK INCF TIME2,F ; Bump up button scan counter
RETLW 0 ; Return to BLDC loop
This actually uses 2 loops with 2 separate button controls (SW1 & 2 first loop, SW3 & 4 second loop).
This was done to experiment with the motor's limits and extra delay is required to slow the loop down to usable periods.
Using a oscilloscope BLDC frequency input was 555.5Hz (the 12 step sequence repeated 555.5 times a second).
The motor under experiment is a 7 phase model helicopter motor running at 3v.
So the highest rpm was found to be 4,761.4 RPM (555.5Hz / 7 phases x 60 seconds).
The PIC could drive it to much higher RPMs, but the motor didn't seem capable of physically spinning any faster.
Once the voltage was increased to 6v, which was the maximum my 3A power pack would allow before cutting out,
A frequency up to 943.3Hz was observed, which translates to 8,085 RPM.
That's over 6 times the rated RPM!
Not bad for a motor rated at max 1,300 RPM
Of course, in a vehicle the motor will in no way require these extreme RPMs (more likely a twentieth of this),
but it proves the humble low spec PIC is quite able to operate fast enough to run a BLDC motor in a real car.
PWM control
In order to modularise things it might be easier to have separate PWM control to the commutation.
Not sure about this as it might end up more expensive, but we need to have all solutions tested before we can pick the best one.
In that light a simple PWM algorithm has been created to test PWM control on a brushed series would motor.
This is much the same as the BLDC controller but with 2 steps and separate delays for each step.
; **********************************
; ** RESET : main boot routine **
; **********************************
RESET MOVLW B'00000111' ; Disable Comparator module's
MOVWF CMCON
BSF STATUS,RP0 ; Switch to register bank 1
; Disable pull-ups
; INT on rising edge
; TMR0 to CLKOUT
; TMR0 Incr low2high trans.
; Prescaler assign to Timer0
; Prescaler rate is 1:256
MOVLW B'11010111' ; Set PIC options (See datasheet).
MOVWF OPTION_REG ; Write the OPTION register.
CLRF INTCON ; Disable interrupts
MOVLW B'11000000' ; RB7 & RB6 are inputs, RB5...RB0 are outputs.
MOVWF TRISB ; Set BLDC sequence port
MOVLW B'11111111' ; all RA ports are inputs
MOVWF TRISA ; Set button port
BCF STATUS,RP0 ; Switch Back to reg. Bank 0
CLRF PORTB ; Reset BLDC sequence port
MOVLW MARKINITIAL ; Get default value for MARKTIMER
MOVWF MARKPERIOD ; Initialise MARKTIMER
MOVLW SPACEINITIAL ; Get default value for SPACETIMER
MOVWF SPACEPERIOD ; Initialise SPACETIMER
GOTO PCMLOOP ; Start the PCM loop (non return)
; ***********************************************
; * PCM Routine which does Mark-Space *
; ***********************************************
PCMLOOP MOVLW SOMEON ; Get bit pattern for PWM HIGH
MOVWF PORTB ; Set output lines
CALL MARKDELAY ; Do intersample delay
NOP ; Give symmetry
NOP ; Give symmetry
MOVLW SOMEOFF ; Get bit pattern for PWM LOW
MOVWF PORTB ; Set output lines
CALL SPACEDELAY ; Do intersample delay
GOTO PCMLOOP ; Repeat
; ***********************************************
; * Button read and period adjustments *
; ***********************************************
MARKDELAY MOVF MARKPERIOD,W ; Get initial count
MOVWF MARKTIMER ; Set the timer
MARKDELAYLOOP DECFSZ MARKTIMER,F ; Count down
GOTO MARKDELAYLOOP ; Repeat
INCFSZ BUTTONTIMER,W ; Count up button scan timer (256 wrap around)
GOTO ENDMARKDELAY ; Jump out if non-zero
GOTO MARKUPDOWN ; Jump to button scan if zero (every 256 delays)
ENDMARKDELAY NOP ; Give symmetry
NOP ; Give symmetry
NOP ; Give symmetry
NOP ; Give symmetry
NOP ; Give symmetry
RETLW 0 ; Return to BLDC loop
MARKUPDOWN BTFSC PORTA,SW1 ; Skip next if SW1 is up
DECFSZ MARKPERIOD,F ; Decrement first timer initial value
GOTO MARKUP ; Jump if first timer initial value is non-zero
INCF MARKPERIOD,F ; Re-increment first timer if zero
MARKUP BTFSC PORTA,SW2 ; Skip next if SW2 is up
INCFSZ MARKPERIOD,F ; Increment first timer initial value
GOTO MARKBACK ; Jump if first timer initial value is non-zero
DECF MARKPERIOD,F ; Re-decrement to 255 first timer if zero
MARKBACK INCF BUTTONTIMER,F ; Bump up button scan counter
RETLW 0 ; Return to BLDC loop
SPACEDELAY MOVF SPACEPERIOD,W ; Get initial count
MOVWF SPACETIMER ; Set the timer
SPACEDELAYLOOP DECFSZ SPACETIMER,F ; Count down
GOTO SPACEDELAYLOOP ; Repeat
INCFSZ BUTTONTIMER,W ; Count up button scan timer (256 wrap around)
GOTO ENDSPACEDELAY ; Jump out if non-zero
GOTO SPACEUPDOWN ; Jump to button scan if zero (every 256 delays)
ENDSPACEDELAY NOP ; Give symmetry
NOP ; Give symmetry
NOP ; Give symmetry
NOP ; Give symmetry
NOP ; Give symmetry
RETLW 0 ; Return to BLDC loop
SPACEUPDOWN BTFSC PORTA,SW3 ; Skip next if SW3 is up
DECFSZ SPACEPERIOD,F ; Decrement first timer initial value
GOTO SPACEUP ; Jump if first timer initial value is non-zero
INCF SPACEPERIOD,F ; Re-increment first timer if zero
SPACEUP BTFSC PORTA,SW4 ; Skip next if SW4 is up
INCFSZ SPACEPERIOD,F ; Increment first timer initial value
GOTO SPACEBACK ; Jump if first timer initial value is non-zero
DECF SPACEPERIOD,F ; Re-decrement to 255 first timer if zero
SPACEBACK INCF BUTTONTIMER,F ; Bump up button scan counter
RETLW 0 ; Return to BLDC loop
END
Now the PIC has a PWM output controller built in as well (called a CCP module), but it is not going to used as yet.
This is for two reasons:
The code for a PWM was created as precursor to the BLDC so it's relatively simple to update it.
The CCP module is a little in-depth and is not required just to build/test the electronics/electrics.
In actual fact there is simple a way of incorporating the PWM into the BLDC commutation.
This can be done by adding in an intermediate space step between each of the 12 "mark" steps producing 24 steps.
The timing of the "mark" BLDC step delay can be varied independently of the "space" step delay, thus giving a PWM.
This, of course, will be low frequency and in fact the frequency is a function of the shaft speed.
Having a low frequency will naturally make the system more efficient as the IGBTs will spend more time on the switched states and less time in the transitional states.
The by-product is that there will be more noise from the motors operation.
This is a desirable thing for the Mass-EV and the Impulse-EV as it is not the target of this project to be the smoothest and quietest experience.
Certainly the Impulse-EV will be designed to have a sense of presence and occasion, rather than a mousey high speed stealth machine.
The BLDC mark to PWM space ratio will vary to provide the torque control.
The mark will be between 0 and 30 degrees of the phase, 0 being no torque and 30 being maximum.
As usual the stator field will be 90 degrees to the rotor field.
This will make it nice and simple to commutate with torque control without the use of the heavy maths involved in Clarke and Park transforms.
This uses double nested loops since the test is using relays and needs to operate slowly
;==========================================================================
; PCMBLDCLOOP: PCM Routine which does BLDC commutation
;==========================================================================
PCMBLDCLOOP MOVLW STEP01 ; Get bit pattern for BLDC step
MOVWF PORTB ; Set output lines
CALL MARKDELAY ; Do intersample delay
NOP ; Give symmetry
NOP ; Give symmetry
MOVLW ALLOFF ; Get bit pattern for PWM LOW
MOVWF PORTB ; Set output lines
CALL SPACEDELAY ; Do intersample delay
NOP ; Give symmetry
NOP ; Give symmetry
. . . steps 02 - 11 removed for clarity
MOVLW STEP12 ; Get bit pattern for BLDC step
MOVWF PORTB ; Set output lines
CALL MARKDELAY ; Do intersample delay
NOP ; Give symmetry
NOP ; Give symmetry
MOVLW ALLOFF ; Get bit pattern for PWM LOW
MOVWF PORTB ; Set output lines
CALL SPACEDELAY ; Do intersample delay
GOTO PCMBLDCLOOP ; Repeat BLDC sequence (2 cycles)
;==========================================================================
; MARKDELAY: Button read and period adjustments
;==========================================================================
MARKDELAY MOVF MARKPERIOD,W ; Get initial count
MOVWF MARKTIMER ; Set the timer
MARKDELAYLOOP MOVF MARKPERIOD,W ; Get initial count
MOVWF MARKINNERTIMER ; Set the timer
MARKINNERLOOP DECFSZ MARKINNERTIMER,F; Count down
GOTO MARKINNERLOOP ; Repeat
DECFSZ MARKTIMER,F ; Count down
GOTO MARKDELAYLOOP ; Repeat
INCFSZ BUTTONTIMER,F ; Count up button scan timer (256 wrap around)
GOTO ENDMARKDELAY ; Jump out if non-zero
GOTO MARKUPDOWN ; Jump to button scan if zero (every 256 delays)
ENDMARKDELAY NOP ; Give symmetry
NOP ; Give symmetry
NOP ; Give symmetry
NOP ; Give symmetry
NOP ; Give symmetry
RETLW 0 ; Return to BLDC loop
MARKUPDOWN BTFSC PORTA,SW1 ; Skip next if SW1 is up
DECFSZ MARKPERIOD,F ; Decrement first timer initial value
GOTO MARKUP ; Jump if first timer initial value is non-zero
INCF MARKPERIOD,F ; Re-increment first timer if zero
MARKUP BTFSC PORTA,SW2 ; Skip next if SW2 is up
INCFSZ MARKPERIOD,F ; Increment first timer initial value
GOTO MARKBACK ; Jump if first timer initial value is non-zero
DECF MARKPERIOD,F ; Re-decrement to 255 first timer if zero
MARKBACK INCF BUTTONTIMER,F ; Bump up button scan counter
RETLW 0 ; Return to BLDC loop
;==========================================================================
; SPACEDELAY: Button read and period adjustments
;==========================================================================
SPACEDELAY MOVF SPACEPERIOD,W ; Get initial count
MOVWF SPACETIMER ; Set the timer
SPACEDELAYLOOP MOVF SPACEPERIOD,W ; Get initial count
MOVWF SPACEINNERTIMER ; Set the timer
SPACEINNERLOOP DECFSZ SPACEINNERTIMER,F ; Count down
GOTO SPACEINNERLOOP ; Repeat
DECFSZ SPACETIMER,F ; Count down
GOTO SPACEDELAYLOOP ; Repeat
INCFSZ BUTTONTIMER,F ; Count up button scan timer (256 wrap around)
GOTO ENDSPACEDELAY ; Jump out if non-zero
GOTO SPACEUPDOWN ; Jump to button scan if zero (every 256 delays)
ENDSPACEDELAY NOP ; Give symmetry
NOP ; Give symmetry
NOP ; Give symmetry
NOP ; Give symmetry
NOP ; Give symmetry
RETLW 0 ; Return to BLDC loop
SPACEUPDOWN BTFSC PORTA,SW3 ; Skip next if SW3 is up
DECFSZ SPACEPERIOD,F ; Decrement first timer initial value
GOTO SPACEUP ; Jump if first timer initial value is non-zero
INCF SPACEPERIOD,F ; Re-increment first timer if zero
SPACEUP BTFSC PORTA,SW4 ; Skip next if SW4 is up
INCFSZ SPACEPERIOD,F ; Increment first timer initial value
GOTO SPACEBACK ; Jump if first timer initial value is non-zero
DECF SPACEPERIOD,F ; Re-decrement to 255 first timer if zero
SPACEBACK INCF BUTTONTIMER,F ; Bump up button scan counter
RETLW 0 ; Return to BLDC loop
END
Merged the PWM code with the BLDC code, total program: size 365 lines including space and comments.
This is the modified BLDC code to include PWM.
If you don't like the stereoscopic 3D just switch it off.
Arduino (ATMEL SBC)
Looking at an Arduino as perhaps an alternative to a PIC.
There's nothing wrong with the PIC, but the AMTEL CPU already has 6 ADCs and 6 PWM outputs plus other DIO.
Ethernet can also be added and even wireless quite cheaply.
This means it's possible to write a simple UI as a http server and operate the thing directly from a PC using a browser.
The project is very Linux friendly and the IDE is quite simple to use.
Some preliminary tests to get a grip on the speed of the CPU:
void setup()
{
// initialize the digital pin as an output.
// Pin 13 has an LED connected on most Arduino boards:
pinMode(13, OUTPUT);
}
// zero gives 2.75uS (363kHz) actually a sawtooth rather than square
// 5 gives 9.1uS (110kHz) 700nS x 2 (15%) is transition
// 10 gives 15.4uS (65kHz) again 700nS x 2 (9%) is transition
// 20 gives 27.5uS (36.3kHz) with a reasonable square
// 100 gives 128uS (7.8kHz)
// 255 gives 325uS (3.1kHz)
// 1000 gives 4.5mS (222Hz) unsigned short
// 10000 gives 22.5mS (44Hz)
// 65535 gives 148mS (6.76Hz) -visible flashing
// 100000 gives 430mS (2.3Hz) unsigned long
static const unsigned char DELAY = 0;
void loop()
{
#if 1
// No delay code gives a period of 810nS (1.23MHz)
PORTB = B00100000; // set the LED on
for (volatile unsigned char intCount = DELAY; intCount > 0; intCount--);
PORTB = B00000000; // set the LED off
for (volatile unsigned char intCount = DELAY; intCount > 0; intCount--);
#else
// No delay code gives a period of 10.8uS (92.6kHz)
digitalWrite(13, HIGH); // set the LED on
// delay(1000); // wait for a second
digitalWrite(13, LOW); // set the LED off
// delay(1000); // wait for a second
#endif
}
A 10MHz 'scope was used so this may be the limitation which showed poor squares.
Checked on a 100MHz 'scope it shows the square is good, so it's just the scope.
The frequency/periods were pretty much right so it seems the 'scope good enough.
It is considerably faster than the PIC from previous experiments.
The same test without any delay code at all ran at 8uS (125kHz) so this is about 10 times faster.
The PIC16F628A is was clocked at 4Mhz, the ATMEGA328P is clocked at 16MHz.
Both are 8-bit CPUs, the PIC has 3.5k flash the ATMEL has 32k.
Price at RS is £1.55 for PIC and £3.22 for the ATMEL (25th July 2012).
Both CPUs are sub-£10 so price is not an issue.
The development kit from Maplin for the PIC is around £35 whereas the base kit for the ATMEL is about £25.
All things considered the ATMEL is a good option.
Controlling a Motor
First pass on the Arduino version:
/*
BLDC with PWM first pass
*/
/* http://www.arduino.cc/en/Reference/PortManipulation
PORTB is digital pin 8 to 13
PORTC is analog input pins
PORTD is digital pins 0 to 7
DDRx is the direction register for PORTx.
DDRD = B11111110; // sets Arduino pins 1 to 7 as outputs, pin 0 as input
DDRD |= B11111100; // this is safer as it sets pins 2 to 7 as outputs
// without changing the value of pins 0 & 1, which are RX & TX
*/
void setup()
{
// initialize the digital pins 7 to 2 as an output.
DDRD |= B11111100;
}
/*
zero gives 2.75uS (363kHz)
5 gives 9.1uS (110kHz)
10 gives 15.4uS (65kHz)
20 gives 27.5uS (36.3kHz)
100 gives 128uS (7.8kHz)
255 gives 325uS (3.1kHz)
1000 gives 4.5mS (222Hz) unsigned short
10000 gives 22.5mS (44Hz)
65535 gives 148mS (6.76Hz) -visible flashing
100000 gives 430mS (2.3Hz) unsigned long
*/
static const unsigned long MARK = 100;
static const unsigned long SPACE = 1000;
static const unsigned char Step[] =
{
B00011010,
B00000000,
B00010010,
B00000000,
B00010110,
B00000000,
B00000110,
B00000000,
B00100110,
B00000000,
B00100100,
B00000000,
B00100101,
B00000000,
B00100001,
B00000000,
B00101001,
B00000000,
B00001001,
B00000000,
B00011001,
B00000000,
B00011000,
B00000000
};
static const unsigned char StepCount = sizeof (Step) / sizeof (char);
void loop()
{
for (unsigned char intCount = 0; intCount < StepCount; intCount++)
{
PORTD = Step[intCount++];
for (volatile unsigned long intDelay = MARK; intDelay > 0; intDelay--);
PORTD = Step[intCount];
for (volatile unsigned long intDelay = SPACE; intDelay > 0; intDelay--);
}
}
Quick test with the MARK at 100 and SPACE at 1000 gives us a good low speed turn.
Also we are starting to hear the motor sound now, which is good.
Using the Sensors
We need a different algorithm to use the Hall inputs.
The diode matrix was based on a 6 step sequence.
Also the electronics is set up for reverse logic on the inputs to reduce circuit complexity.
/*
Software replacement of the diode matrix
Just a simple loop to iterate through the possible inputs and map the output.
Sample period should be shorter than 100uS.
*/
typedef struct
{
unsigned char Sensor;
unsigned char Phase;
}
PhaseMapSpec;
static const unsigned char SensorsMask = B11111100;
// Sensor input is reverse logic
static const PhaseMapSpec PhaseMap[] =
{
// Sensor Phase
// UUVVWW
// 654321 -+-+-+
{B01111100, B00010010},
{B10111100, B00000110},
{B11011100, B00100100},
{B11101100, B00100001},
{B11110100, B00001001},
{B11111000, B00011000}
};
static const unsigned char PhaseMapSize = sizeof (PhaseMap) / sizeof (PhaseMapSpec);
void setup()
{
// PORTB pins 5 to 0 (DIO13 - DIO08) as field control outputs.
DDRB |= B00111111;
// PORTD pins 7 to 2 (DIO07 - DIO02) as sensor inputs.
DDRD &= B00000011;
}
void loop()
{
for (unsigned char intCount = 0; intCount < PhaseMapSize; intCount++)
{
if (PhaseMap[intCount].Sensor == (PIND & SensorsMask))
{
PORTB = PhaseMap[intCount].Phase;
break;
}
}
}
Adding speed controls
A simple way of testing the PWM control is needed using analogue pots.
This is using the original 12-step sequence with mark-space in the sequence to make it 24-step.
So we added code to control the basic mark/space from the 2 pots into the raw stepper code:
void setup()
{
// initialize the digital pins 5 to 0 as an output.
DDRB |= B00111111;
}
static unsigned long Mark = 0;
static unsigned long Space = 0;
static const unsigned char Step[] =
{
B00011010,
B00000000,
B00010010,
B00000000,
B00010110,
B00000000,
B00000110,
B00000000,
B00100110,
B00000000,
B00100100,
B00000000,
B00100101,
B00000000,
B00100001,
B00000000,
B00101001,
B00000000,
B00001001,
B00000000,
B00011001,
B00000000,
B00011000,
B00000000
};
static const unsigned char StepCount = sizeof (Step) / sizeof (char);
void loop()
{
for (unsigned char intCount = 0; intCount < StepCount; intCount++)
{
Space = (analogRead(0) << 4);
Mark = (analogRead(1));
PORTB = Step[intCount++];
for (volatile unsigned long intDelay = Mark; intDelay > 0; intDelay--);
PORTB = Step[intCount];
for (volatile unsigned long intDelay = Space; intDelay > 0; intDelay--);
}
}
This worked OK, but obviously careful control the PWM is required or the step sequence will be lost, since there is still no sensor input.
First complete controller program
A merge of the two produces something which gives proper sequencing from sensors and PWM control using analogue pots:
// Initial test with sensors and variable PWM from pots
typedef struct
{
unsigned char Sensor;
unsigned char Phase;
}
PhaseMapSpec;
static unsigned long Mark = 0;
static unsigned long Space = 0;
static const unsigned char SensorsMask = B11111100;
// Sensor input is reverse logic
static const PhaseMapSpec PhaseMap[] =
{
// Sensor Phase
// UUVVWW
// 654321 -+-+-+
{B01111100, B00010010},
{B10111100, B00000110},
{B11011100, B00100100},
{B11101100, B00100001},
{B11110100, B00001001},
{B11111000, B00011000}
};
static const unsigned char PhaseMapSize = sizeof (PhaseMap) / sizeof (PhaseMapSpec);
void setup()
{
// PORTB pins 5 to 0 (DIO13 - DIO08) as field control outputs.
DDRB |= B00111111;
// PORTD pins 7 to 2 (DIO07 - DIO02) as sensor inputs.
DDRD &= B00000011;
}
static unsigned char LastSensor = SensorsMask;
static unsigned char ThisSensor = SensorsMask;
void loop()
{
// Hold the "mark" until a sensor change
for (unsigned long intCount = Mark; ThisSensor == LastSensor && intCount > 0; intCount--)
{
ThisSensor = (PIND & SensorsMask);
if (ThisSensor == SensorsMask)
ThisSensor = LastSensor;
}
LastSensor = ThisSensor;
// Read the pots
Space = (analogRead(0) << 4);
Mark = (analogRead(1) << 4);
// Add a "space"
PORTB = 0;
for (volatile unsigned long intDelay = Space; intDelay > 0; intDelay--);
// Then do a "mark" and hold until the next sensor change
for (unsigned char intCount = 0; intCount < PhaseMapSize; intCount++)
{
if (PhaseMap[intCount].Sensor == ThisSensor)
{
PORTB = PhaseMap[intCount].Phase;
break;
}
}
}
This worked quite well at higher speeds but was a bit unstable in very low speeds.
There is a latch set by a read to ADCL which is released by a read to ADCH. The Analog to Digital Converter (ADC) has more, so you need to be careful of the read order.
Now the main loop doesn't actually read the pots just uses a stored value set by the ISR, so it is fast again.
The only issue here is the ISR obviously will cause a small delay when it fires asynchronously,
but this will still be much less that waiting for a read to complete.
So this is the full program:
// Sensors with variable PWM from pots
// Sensor input is reverse logic
#define ADCREAD(Index) (((unsigned short)ADCRead[(Index)].HighByte << 12) | \
((unsigned short)ADCRead[(Index)].LowByte << 4))
typedef struct
{
unsigned char HighByte;
unsigned char LowByte;
}
ADCReadSpec;
static const unsigned char SensorsMask = B11111100;
static const unsigned char ADMUXFLAGS = (1 << REFS0); // ADC reference to AVCC
static const unsigned char ADCSRAFLAGS = (1 << ADEN) | (1 << ADSC); // Enable interrupts and start A2D Conversion
static ADCReadSpec ADCRead[2] =
{
{0, 0},
{0, 0}
};
// Create a sparse array populated in setup().
// This will take more space, but will work much faster
static unsigned char PhaseMap[256];
static unsigned char LastSensor = SensorsMask;
static unsigned char ThisSensor = SensorsMask;
static unsigned char LastADCRead = 255;
void setup()
{
DDRB |= B00111111;
DDRD &= B00000011;
// UUVVWW
// 654321 -+-+-+
PhaseMap [B01111100] = B00010010;
PhaseMap [B10111100] = B00000110;
PhaseMap [B11011100] = B00100100;
PhaseMap [B11101100] = B00100001;
PhaseMap [B11110100] = B00001001;
PhaseMap [B11111000] = B00011000;
PhaseMap [0] = 0;
sei(); // Enable Global Interrupts
ADCSRA |= (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);
ADCSRA |= (1 << ADEN);
ADCSRA |= (1 << ADIE);
ADCSRA |= (1 << ADSC);
}
ISR (ADC_vect)
{
LastADCRead = ADMUX & B00000111;
ADCRead[LastADCRead].LowByte = ADCL;
ADCRead[LastADCRead].HighByte = ADCH;
ADMUX = (LastADCRead == 0) | ADMUXFLAGS; // Select other channel
ADCSRA |= ADCSRAFLAGS;
}
void loop()
{
// Read the pots
unsigned short Mark = ADCREAD(1);
unsigned short Space = ADCREAD(0);
for (unsigned short intCount = Mark; ThisSensor == LastSensor && intCount > 0; intCount--)
{
ThisSensor = (PIND & SensorsMask);
if (ThisSensor == SensorsMask)
ThisSensor = LastSensor;
LastSensor = ThisSensor;
// Read the pots
Space = (analogRead(0) << 4);
Mark = (analogRead(1) << 4);
// Add a "space"
PORTB = 0;
for (volatile unsigned long intDelay = Space; intDelay > 0; intDelay--);
// Then do a "mark" and hold until the next sensor change
for (unsigned char intCount = 0; intCount < PhaseMapSize; intCount++)
{
if (PhaseMap[intCount].Sensor == ThisSensor)
{
PORTB = PhaseMap[intCount].Phase;
break;
}
}
}
This worked quite well at higher speeds but was a bit unstable in very low speeds.
There is a latch set by a read to ADCL which is released by a read to ADCH. The Analog to Digital Converter (ADC) has more, so you need to be careful of the read order.
Now the main loop doesn't actually read the pots just uses a stored value set by the ISR, so it is fast again.
The only issue here is the ISR obviously will cause a small delay when it fires asynchronously,
but this will still be much less that waiting for a read to complete.
This appears to control the torque very well, it uses the power efficiently and uses low frequency PWM.
It still is a bit unstable at very low speeds.
The code does not vary the phase angle (fixed at 60 degrees) and still uses the 6-step algorithm inherited from the diode matrix.
The phase angle really should be set to 90 degrees which really needs the 12-step algorithm.
Some ratiometric hall effect devices have been ordered so we will test some analogue angle code as well.
ReadReceiver looks interesting as a way of using interrupts instead of loop timers.
Also to have a timer interrupt set to about half second for serial IO to receive commands and send diagnostics.
Also really could improve this with moving average algorithm for reading the pots and interrupt based sensor loop (PinChangeIntExample and Quick Reference).
And this is the CPU datasheet.
Now the interrupt system for this CPU seems straight forward the software should be converted to a fully pre-emptive model.
This way the timing is much more reliable and a user IO interface can be implemented
without needing the carefully timed co-operative model used in the PIC.
The main loop now just reads passive attributes and sends them out USB serial every second.
Possibly can receive commands in from USB serial and setting attributes.
Might be betting to start a simple diode matrix replacement again with some fresh code.
This was the revised pre-emptive diode matrix replacement:
/*
Software replacement of the diode matrix
Uses pin change interrupts to set the phase output.
Should respond in a more predictable way, closer to the electrical version
*/
static const unsigned char SENSORMASK = B11111100;
// Create a sparse array populated in setup().
// This will take more space, but will work much faster
static unsigned char PhaseMap[256];
void setup()
{
Serial.begin (115200);
Serial.println ("SimpleMatrix, "__DATE__" ("__TIME__")");
// PORTB pins 5 to 0 (DIO13 - DIO08) as field control outputs.
DDRB |= B00111111;
// PORTD pins 7 to 2 (DIO07 - DIO02) as sensor inputs.
DDRD &= B00000011;
// Reset all patterns to no field output
for (unsigned short Count = 0; Count < 256; Count++)
PhaseMap [Count] = 0;
// UUVVWW
// 654321 -+-+-+
PhaseMap [B01111100] = B00010010;
PhaseMap [B10111100] = B00000110;
PhaseMap [B11011100] = B00100100;
PhaseMap [B11101100] = B00100001;
PhaseMap [B11110100] = B00001001;
PhaseMap [B11111000] = B00011000;
//-- Sensor interrupt
// Pin change interrupt on sensor inputs
// This corresponds to PCINT2_vect
PCICR |= (1 << PCIE2);
PCMSK2 |= SENSORMASK;
sei(); // Enable Global Interrupts
}
// Sensors (PORTB) interrupt
// Fires each time the sensor pattern changes
ISR (PCINT2_vect)
{
// Simply map the field output pattern to the sensor input pattern
PORTB = PhaseMap[PIND & SENSORMASK];
}
void loop()
{
delay (1000); // Do nothing
}
Some quick calculations on the Field Control page show that
the software needs to be able to run the motor at about 460 field rotations per second for 100mph.
So code was added to see the rotational speed and the quality of the sensor information:
Also the ISR was adapted to ignore empty reads (when the sensors are not picking up the magnets).
460 field rotations per second is expected to be attained.
In 6 step mode this is 6 falling edges (reverse logic) so 460 x 6 = 2760 sensor interrupts per second.
So interrupts every 362.32uS with a new sensor update are expected.
In actual fact interrupts will occur twice but the rising edges will be throw away .
Since we have a serial interface which is bidirectional this allows us to talk to the controller from the PC.
This is useful to send commands to alter the state, etc:
static const unsigned short FIXEDSPACETEST = 2650;
static unsigned short SpaceTest = FIXEDSPACETEST;
static char KeyPress = '\0'; // for incoming serial data
. . .
// Fires each time the sensor pattern changes
ISR (PCINT2_vect)
{
. . .
PORTB = 0;
ICR1 = SpaceTest;
. . .
}
. . .
void loop()
{
delay (1000);
// Check for commands
if (Serial.available() > 0)
{
// read the incoming byte:
KeyPress = (char)Serial.read();
// say what you got:
Serial.print ("I received: '");
Serial.print (KeyPress);
Serial.println ("'");
switch (KeyPress)
{
case 'u':
SpaceTest >>= 1;
break;
case 'd':
SpaceTest <<= 1;
break;
case 's':
PCICR &= ~(1 << PCIE2); // stop sensor reads
TCCR1B &= ~TIMERPRESCALEMASK; // stop timer
PORTB = 0; // shut off all outputs
break;
case 'r':
PCICR |= (1 << PCIE2); // start sensor reads
// Initiate movement
if (SENSORMASK == (SensorRead = PIND & SENSORMASK))
SensorRead = B00010010; // Just give it something
PORTB = PhaseMap[SensorRead];
break;
}
}
. . .
}
This works better in a proper terminal program like gtkterm or minicom as the Arduino stuff requires you to press return to actually send.
So you can now press:
'u' to half the space time (ADCs are currently blown)
'd' to double the space time
's' to stop the motor, allowing it to run down
'r' to run the motor
There's a delay max 1 second due to the polling loop, so a serial read ISR might be better.
There is still the issue that the motor and CD etc are very light so a small kick can send it off into fast spin.
The rotary parts have a very small inertia.
Another problem is that the motor doesn't turn when the controller is powered up.
This is because the sensors don't fire interrupts until a change occurs, which doesn't happen when it is stationary.
Also we need to modify the code to allow experimentation.
This required modification of the HardwareSerial Arduino module to comment out the USART_RX_vect section (in a local copy).
Hence Serial. is renamed to COM.
;==========================================================================
; PICMONO2: Monostable second testing loop
;==========================================================================
PICMONO2 MOVLW B'00000010' ; Get bit pattern for LED ON and 555RST LOW
MOVWF PORTB ; Set output lines
MOVLW B'00000011' ; Get bit pattern for LED ON and 555RST HIGH
MOVWF PORTB ; Set output lines
WAITMARK BTFSC PORTA,0 ; Read Monostable input (RA0)
GOTO WAITMARK
NOP ; Symmetric
NOP ; Symmetric
MOVLW B'00000000' ; Get bit pattern for LED OFF and 555RST LOW
MOVWF PORTB ; Set output lines
MOVLW B'00000001' ; Get bit pattern for LED OFF and 555RST HIGH
MOVWF PORTB ; Set output lines
WAITSPACE BTFSC PORTA,0 ; Read Monostable input (RA0)
GOTO WAITSPACE
GOTO PICMONO2 ; Return
This would, in reality, have 2 timer circuits one for mark and one for space.
;==========================================================================
; PICMONO2: Monostable second testing loop
;==========================================================================
PICMONO2 MOVLW B'10000001' ; Get bit pattern for LED ON (RB01) and MARKTRIG LOW (RB06)
MOVWF PORTB ; Set output lines
WAITMARKRESET BTFSS PORTA,6 ; Read Monostable input (RA6)
GOTO WAITMARKRESET ; Loop until high
MOVLW B'11000001' ; Get bit pattern for LED ON (RB01) and MARKTRIG HIGH (RB06)
MOVWF PORTB ; Set output lines
WAITMARK BTFSC PORTA,6 ; Read Monostable input (RA6)
GOTO WAITMARK ; Loop until low
NOP ; Symmetry
NOP ; Symmetry
MOVLW B'01000000' ; Get bit pattern for LED OFF (RB01) and SPACETRIG LOW (RB07)
MOVWF PORTB ; Set output lines
WAITSPACERESET BTFSS PORTA,7 ; Read Monostable input (RA7)
GOTO WAITSPACERESET ; Loop until high
MOVLW B'11000000' ; Get bit pattern for LED OFF (RB01) and SPACETRIG HIGH (RB07)
MOVWF PORTB ; Set output lines
WAITSPACE BTFSC PORTA,7 ; Read Monostable input (RA7)
GOTO WAITSPACE ; Loop until low
GOTO PICMONO2 ; Return
END
;==========================================================================
; PICMONO2: Monostable second testing loop
;==========================================================================
PICMONO2 BSF PORTB,0 ; Set LED ON (RB01)
BCF PORTB,6 ; Set MARKTRIG LOW (RB06)
WAITMARKHIGH BTFSS PORTA,6 ; Read Monostable input (RA6)
GOTO WAITMARKHIGH ; Loop until high
BSF PORTB,6 ; Set MARKTRIG HIGH (RB06)
WAITMARKLOW BTFSC PORTA,6 ; Read Monostable input (RA6)
GOTO WAITMARKLOW ; Loop until low
NOP ; Symmetry
NOP ; Symmetry
BCF PORTB,0 ; Set LED OFF (RB01)
BCF PORTB,7 ; Set SPACETRIG LOW (RB07)
WAITSPACEHIGH BTFSS PORTA,7 ; Read Monostable input (RA7)
GOTO WAITSPACEHIGH ; Loop until high
BSF PORTB,7 ; Set SPACETRIG HIGH (RB07)
WAITSPACELOW BTFSC PORTA,7 ; Read Monostable input (RA7)
GOTO WAITSPACELOW ; Loop until low
GOTO PICMONO2 ; Return
PIC software created in Piklab,
Arduino software created in the Arduino IDE,
animations created in qcad/librecad,
plots and graphs GNumeric,
images edited in gimp,
flowcharts created in LibreOffice Draw.