这次使用的GD系列的芯片来做电机的驱动。然后对使用GD芯片做个总结。这次做的是一个光伏机器人项目。主要用在光伏板上的机器人,通过驱动电机来清理光伏板。
1.PWM配置
电机驱动使用的是PWM波来驱动。所以我们首先先用GD芯片来产生PWM波。首先下面是PWM的定时器配置。
void PWM_Timer_Config(uint32_t psr, uint32_t arr, uint32_t duty)
{
timer_oc_parameter_struct timer_ocintpara;
timer_parameter_struct timer_initpara;
timer_break_parameter_struct timer_breakpara;
rcu_periph_clock_enable(RCU_TIMER0);
timer_deinit(TIMER0);
/* TIMER0 configuration */
timer_initpara.prescaler = psr;
timer_initpara.alignedmode = TIMER_COUNTER_EDGE;
timer_initpara.counterdirection = TIMER_COUNTER_UP;
timer_initpara.period = arr;
timer_initpara.clockdivision = TIMER_CKDIV_DIV1;
timer_initpara.repetitioncounter = 0;
timer_init(TIMER0,&timer_initpara);
/* CH0 configuration in PWM mode */
timer_ocintpara.outputstate = TIMER_CCX_ENABLE;
timer_ocintpara.outputnstate = TIMER_CCXN_ENABLE;
timer_ocintpara.ocpolarity = TIMER_OC_POLARITY_HIGH;
timer_ocintpara.ocnpolarity = TIMER_OCN_POLARITY_HIGH;
timer_ocintpara.ocidlestate = TIMER_OC_IDLE_STATE_HIGH;
timer_ocintpara.ocnidlestate = TIMER_OCN_IDLE_STATE_HIGH;
timer_channel_output_config(TIMER0,TIMER_CH_0,&timer_ocintpara);
timer_channel_output_pulse_value_config(TIMER0,TIMER_CH_0,duty);
timer_channel_output_mode_config(TIMER0,TIMER_CH_0,TIMER_OC_MODE_PWM0);
timer_channel_output_shadow_config(TIMER0,TIMER_CH_0,TIMER_OC_SHADOW_DISABLE);
timer_breakpara.runoffstate = TIMER_ROS_STATE_DISABLE;
timer_breakpara.ideloffstate = TIMER_IOS_STATE_DISABLE ;
timer_breakpara.deadtime = 5;
timer_breakpara.breakpolarity = TIMER_BREAK_POLARITY_HIGH;
timer_breakpara.outputautostate = TIMER_OUTAUTO_ENABLE;
timer_breakpara.protectmode = TIMER_CCHP_PROT_0;
timer_breakpara.breakstate = TIMER_BREAK_ENABLE;
timer_break_config(TIMER0,&timer_breakpara);
//timer_break_enable(TIMER0);
timer_primary_output_config(TIMER0,ENABLE);
/* auto-reload preload enable */
timer_auto_reload_shadow_enable(TIMER0);
timer_enable(TIMER0);
}
从配置中可以算出产生的PWM的频率。时钟是72MHz的,比如设置 PWM_Timer_Config(72 - 1,200 - 1,40);
那么可以算出频率是 72M / 72 * 200 = 5000Hz 周期是200us。设置占空比: TIMER_CH0CV(TIMER0) = 40; 这样设置的占空比就是20%。这个就是往比较寄存器中存入相应的值,就会和PWM周期进行比较,得出相应的占空比。
breakpara是设置死区时间,死区时间只能设置0-255。
但是电机启动和降速是不能很快的,所以要软启动和软刹车。这样的话PWM频率要慢慢变化。
刚开始想法是每隔一段时间放进去一个比较值,但是发现不行。后来想着每次运行到这就++,但是这样加速时间太慢,而且这和代码整体有关。
最后想着还是先用线性递增的函数来表示,其实如果想用更平滑的启动或者减速,可以用曲线。
uint16_t Calculate_Accelerate_time()
{
uint16_t cal_time = 0;
cal_time = (save_p.Run_Speed/100.0) * MOTOR_HIGHEST_SPEED * 50.0;
cal_time = cal_time - 1000;
return cal_time;
}
void Motor_Accelerate()
{
static uint32_t time_count = 0;
if(time_count == 0)
time_count = tc_GetTimerCount();
else if(tc_GetTimerCount() - time_count <= Calculate_Accelerate_time())
{
pwmvalue = 0.02 * (tc_GetTimerCount() - time_count) + MOTOR_INITALSPEED;
TIMER_CH0CV(TIMER0) = pwmvalue;
}
else
{
time_count = 0;
}
if(pwmvalue == finial_speed)
{
TIMER_CH0CV(TIMER0) = pwmvalue;
time_count = 0;
Set_Motor_State(MOTOR_RUN);
}
}
可以用线性函数来表示,斜率可以自己定。然后可以先试着看看给电机多少占空比才会转,就设置为初始值。因为WEB端是可以设置占空比的,所以接收到的数据解析之后要计算加速时间,不同占空比的加速时间不同。
当电机达到预定的速度之后就匀速运行。减速和这个类似,但有点不同,后面说。
2.485通信
这次485虽然是两个口,但是其实是互通的,配置好一个就可以的。
还有就是别忘了要使能接收和发送,不然不能收发数据。其他配置和串口一样。
3.电机驱动
这是这次电机驱动的原理图,使用的是半桥结构,并没有用到常用的H桥结构。左边两路是PWM输入,上面是PWM波,下面是互补的PWM波。右下角是三个继电器来控制电机的正反转和停止的。右上角是电机的电源。
EG2132是电机驱动芯片。
中间两个是NMOS,判断条件是:不论是PMOS还是NMOS,都是P->N,所以下面是P,左边的N。NMOS是给高电平是导通,给低电平时截止。PMOS是刚好相反。
三极管判断和这个类似,右下方是NPN型三极管,给高就会有电流通过。
3.1电机正转
假定现在如图所示的电机的正转方向,那么就可以从输入端给占空比缓慢变大的PWM,那么同时互补的PWM则是缓慢变小。当上半桥也就是PWM输入部分为高时,上面的MOS管是导通的,同时下面MOS管截止。此时若控制BRK这个管脚,使其吸合,
那么电机就实现了正转。
3.2电机软刹车(动态刹车)
当电机需要刹车时,为了保护电机,不能直接就刹车,而是应该先软刹车,然后再硬刹车。软刹车就是在运行过程中先将速度降下来,然后停止输出PWM波。
3.3电机硬刹车(静态刹车)
在上面这个基础上,因为两路PWM都没有输入,将互补的PWM的信号直接拉高,这样中间两个MOS管中间的点就是直接连接到地了。然后关闭控制电机的继电器,然后延时十几ms之后,再将互补的PWM波置为0。
3.4 总结
总结一下刹车过程。
- 电机减速
- 降到很低的速度之后,停止输出PWM波
- 将互补通道的信号置为高电平,然后关闭继电器,延时一些时间之后,再置为低电平。
void Motor_Moderate()
{
static uint32_t speed_low_time = 0;
if(speed_low_time == 0)
{
speed_low_time = tc_GetTimerCount();
}
else if(tc_GetTimerCount() - speed_low_time <= Calculate_Stop_time()){
pwmvalue = -1 * (tc_GetTimerCount() - speed_low_time) + finial_speed;
TIMER_CH0CV(TIMER0) = pwmvalue;
}
else
{
speed_low_time = 0;
}
if(pwmvalue == MOTOR_INITALSPEED)
{
TIMER_CH0CV(TIMER0) = pwmvalue;
//breakin½ÅÖÃλ
gpio_bit_set(GPIOB,GPIO_PIN_11);
speed_low_time = 0;
//½«#PWMÖÃΪ¸ß
gpio_init(GPIOB, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_13);
gpio_bit_set(GPIOB,GPIO_PIN_13);
Set_Motor_State(MOTOR_HARDBREAK);
}
}
void Motor_HardBreak()
{
static uint32_t stop_time =0;
gpio_bit_reset(GPIOB,GPIO_PIN_0);
if(stop_time == 0)
stop_time = tc_GetTimerCount();
else if(tc_GetTimerCount() - stop_time > 10)
{
gpio_init(GPIOB, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_13);
gpio_bit_reset(GPIOB,GPIO_PIN_13);
timer_primary_output_config(TIMER0,DISABLE);
stop_time = 0;
Motor_ifReverse();
}
}
4.电流采集(ADC)
在电机运行过程中,需要采集电机运行的电流,这样能保证电机的正常运行。
ADC采集过程中,会采集到数据,这个数值是0-4095之间。这个对应的电压在0-3.3V之间,所以可以通过比例可以得出相应的电压。
将得到的电压除以放大倍数,然后再除以电阻的阻值就是电机的电流。
void rcu_config()
{
rcu_periph_clock_enable(RCU_GPIOA);
rcu_periph_clock_enable(RCU_ADC0);
gpio_init(GPIOA, GPIO_MODE_AIN, GPIO_OSPEED_50MHZ, GPIO_PIN_5);
rcu_adc_clock_config(RCU_CKADC_CKAPB2_DIV8);
}
void adc_config()
{
rcu_config();
adc_deinit(ADC0);
adc_mode_config(ADC_MODE_FREE);
adc_data_alignment_config(ADC0, ADC_DATAALIGN_RIGHT);
adc_channel_length_config(ADC0, ADC_INSERTED_CHANNEL, 1);
adc_inserted_channel_config(ADC0, 0, ADC_CHANNEL_5, ADC_SAMPLETIME_55POINT5);
adc_external_trigger_source_config(ADC0,
ADC_INSERTED_CHANNEL,ADC0_1_2_EXTTRIG_INSERTED_NONE);
/* ADC external trigger enable */
adc_external_trigger_config(ADC0, ADC_INSERTED_CHANNEL, ENABLE);
adc_flag_clear(ADC0,ADC_FLAG_EOIC);
/* enable ADC interface */
adc_enable(ADC0);
delay_us(300);
/* ADC calibration and reset calibration */
adc_calibration_enable(ADC0);
}
这次ADC采集是每隔200ms采集一次,使用的是软件中断。是采集11次,然后去掉不符合的电流值,将11个值从小到大排序,去掉最大的和最小的,然后计算平均值。
uint16_t ADC_Filter()
{
uint8_t i,j;
uint16_t temp;
uint16_t sum = 0;
for(i = 0;i < INSERTED_DATA_BUFF_NUM - 1;i++)
{
for(j = 0;j < INSERTED_DATA_BUFF_NUM - i;j++)
{
if(ADC_inserted_data_buff[j] > ADC_inserted_data_buff[j + 1])
{
temp = ADC_inserted_data_buff[j];
ADC_inserted_data_buff[j] = ADC_inserted_data_buff[j + 1];
ADC_inserted_data_buff[j + 1] = temp;
}
}
}
for(i = 1; i < INSERTED_DATA_BUFF_NUM;i++)
{
sum += ADC_inserted_data_buff[i];
}
return (sum / (INSERTED_DATA_BUFF_NUM - 2));
}
uint16_t ADCValue_Put_Buf(uint16_t data)
{
static uint8_t count = 0;
float average_voltage = 0;
if(data > 0x0064)
ADC_inserted_data_buff[count++] = data;
if(count == INSERTED_DATA_BUFF_NUM)
{
average_voltage = ADC_Filter();
count = 0;
memset(ADC_inserted_data_buff,0x00,INSERTED_DATA_BUFF_NUM);
return average_voltage;
}
}
ADC的滤波算法还有很多种,具体选择看这个,ADC滤波算法。
5.其他需要注意点
5.1按键
只要涉及到按键的判断,最好加入防抖动。
uint8_t Judge_Shake()
{
static uint8_t key_up = 1;
if(key_up && gpio_input_bit_get(GPIOB,GPIO_PIN_3) == RESET)
{
key_up = 0;
JlinkPrintf("LS_GET.......\n");
delay_ms(15);
return 1;
}
else if(gpio_input_bit_get(GPIOB,GPIO_PIN_3))
key_up = 1;
return 0;
}
5.2 Flash读写
嵌入式会经常涉及到Flash的读写,那么Flash的地址一定要选对。最好能从大的往小的选,不然有时候选择的地址会和代码段重复。导致地址里面的数据会被修改。
在map文件中可以看flash和ram的大小。
有一次出现这个情况,我将一些参数存在22k的地方,然后有一段调用某个函数的地方我将它注释了,得出的结果和想象中一致。当打开注释,调用这个函数时候,flash里面的值就被修改了。
当时不知道是什么情况,后来经过师兄指导之后知道,没调用这个函数之前,在map文件中flash用了21.83k,调用这个函数之后,编译之后,flash用了22.08K,说明这里冲突了。