6. 直流有刷驱动板电流电压采集

野火使用MOS管搭建的直流有刷驱动板做到了信号完全隔离,其他驱动板基本都只是使用光耦隔离了控制信号, 并没有对ADC采样电路进行隔离,野火不仅使用光耦对控制信号进行了隔离, 还使用AMC1200SDUBR隔离运放对ADC采样电路进行了隔离。

6.1. 电流采样电路

如下图所示是电流采样电路,在电机驱动电路中串入一个0.02Ω、2W的采样电阻,将电流信号转换成电压信号, 再经过隔离运放放大8倍后差分输出,使用普通运放将差分输出转换成单端输出给瑞萨RA的ADC采样通道。

电流采样电路

从上图中我们可以知道是一个负反馈电路,那么根据虚短和虚断可以知道Up=Un, p点和n点没有电流到运放的5脚和6脚,可以得:

差分转单端输出

R61与后面的电容组成RC滤波电路,R61上流过的电流很小,压降也小,可以忽略不计,Vo等于Vcurrent_adc

将(1)式和(2)式整理可得:

差分转单端输出整理

因为Up=Un,所以有:

差分转单端输出合并

其中R52=R51=R53=R50=10KΩ, 将R52、R51、R53和R50阻值带入上式化简可得:

差分转单端输出化简

因为隔离运放将Vi放大8倍后输出,所以有U7-U6=8*Vi, 带入上式可得:

差分转单端输出结果

在下图中使用电压比较器LMV331SE实现10A过流保护电路,电流采样电路中Vi经过隔离运放和普通运放后变成Vcurrent_adc输入到下图比较器的IN-, 当IN-的电压超过IN+时比较器的OUT将输出低电平到锁存器中,锁存器对输入信号进行锁存并输出到与门。

电压比较器

光耦隔离部分电路图如下图所示。

光耦隔离部分

与门输入输出与MOS管状态真值表如下表所示。

与门输入输出与MOS管状态真值表

A

B

Y

MOS

H

H

H

可导通

H

L

L

关断(过流保护)

L

H

L

关断(单片机控制关断)

L

L

L

关断(单片机控制关断,过流保护)

6.2. 电压采样电路

如下图所示是电源电压采样电路,在电源电压上并联R18和R19的串联电阻,R19两端的电压作为隔离运放的输入, 再经过隔离运放放大8倍后差分输出,使用普通运放将差分输出转换成单端输出,连接到瑞萨RA的ADC采样通道。 隔离运放的输入电压为Vi,则有:Vi/R19=POWER/(R18+R59+R19),带入电阻值可得:Vi=POWER/37, 通过上一节中电流采样电流的计算方法可以计算得到POWER_ADC=POWER/37+1.24,不同的是,电压检测部分的隔离运放是没有放大的。

有刷电源电压采集电路图

6.3. 硬件连接

本章实验需要连接开发板和驱动板,这里给出接线表。

6.3.1. MOS管搭建驱动板

电机与MOS管搭建驱动板连接见下表所示。

电机与MOS管搭建驱动板连接

电机

MOS管搭建驱动板

M+

M+

5V

编码器电源:+

GND

编码器电源:-

A

A

B

B

M-

M-

MOS管搭建驱动板与主控板连接见下表所示。

MOS管搭建驱动板与主控板连接

MOS管搭建驱动板

主控板

PWM1

PE10

PWM2

PE11

SD

PB15

Current_ADC

PA01

POWER_ADC

PA03

电源输入:5V

5V

电源输入:GND

GND

推荐使用配套的牛角排线直接连接驱动板和主控板,如使用牛角排线,注意缺口方向。连接开发板的那端,请连接在“直流有/无刷电机驱动接口1”上。

6.4. 在瑞萨RA中实现电流电压采集

从第一节电流采样电路中,我们可以知道,想要对电流进行采集,需要将电流信号转换为电压信号。 我们通过硬件部分完成了对该信号转换、放大处理,这样一来就可以很方便的在瑞萨RA中使用ADC外设对该信号进行采集。 在瑞萨RA中采集到了数据,最终再通过的一些数据的处理,我们就可以得到所需的电流值。 当然同理可得,我对电压信号的采集也是类似地,下面我们看代码如何进行这部分的处理。

6.4.1. 软件设计

6.4.1.1. 编程要点

  1. 初始化ADC并进行数据的获取

  2. 编写函数对采集得到的数据进行处理

  3. 编写获取最终电流值的函数

  4. 测试代码

6.4.1.2. 新建工程

我们直接在上一章节的的 “200_Motor_BDC_PWM_Control” 例程的基础上修改程序。

对于 e2 studio 开发环境:

拷贝一份我们之前的 e2s 工程 “200_Motor_BDC_PWM_Control”, 然后将工程文件夹重命名为 “201_Motor_BDC_Current_Voltage_Acquisition”,最后再将它导入到我们的 e2 studio 工作空间中。

对于 Keil 开发环境:

拷贝一份我们之前的 Keil 工程 “200_Motor_BDC_PWM_Control”, 然后将工程文件夹重命名为 “201_Motor_BDC_Current_Voltage_Acquisition”,并进入该文件夹里面双击 Keil 工程文件,打开该工程。

工程新建好之后,在工程根目录的 “src” 文件夹下面新建 “adc” 文件夹, 再进入 “adc” 文件夹里面新建源文件和头文件:“motor_v_c_acquisition.c” 和 “motor_v_c_acquisition.h”。 工程文件结构如下。

文件结构
201_Motor_BDC_Current_Voltage_Acquisition
├─ ......
└─ src
   ├─ adc
   │  ├─ motor_v_c_acquisition.c    //新建文件
   │  └─ motor_v_c_acquisition.h    //新建文件
   ├─ beep
   │  ├─ bsp_beep.c
   │  └─ bsp_beep.h
   ├─ debug_uart
   │  ├─ bsp_debug_uart.c
   │  └─ bsp_debug_uart.h
   ├─ gpt
   │  ├─ bsp_gpt_pwm_output.c
   │  └─ bsp_gpt_pwm_output.h
   ├─ key
   │  ├─ bsp_key_irq.c
   │  └─ bsp_key_irq.h
   ├─ led
   │  ├─ bsp_led.c
   │  └─ bsp_led.h
   ├─ motor_controls
   │  ├─ bsp_motor_control.c
   │  └─ bsp_motor_control.h
   └─ hal_entry.c

6.4.1.3. FSP配置

下面以野火启明6T2开发板为例来讲解相关的 FSP 配置。

根据原理图所示,需要打开 AN001 以及 AN003 引脚用作电压和电流的采集引脚。 ADC相关知识请参考《瑞萨RA系列FSP库开发实战指南》,这里不过多赘述。 这里来讲解使用FSP库进行电压电流采集时如何更便捷的使用偏置值计算,如下图:

偏置表

在图中,除了设置ADC模块的名称以及回调函数,还有两个特殊的表,分别代表 “用户偏移功能” 以及 “用户增益功能”, 用户偏移调整功能可在 A/D 转换数据中添加或减去一个常量值。Virtual channels 从用户指定的值表中选择一个偏移量。 用户增益调整功能将 A/D 转换数据乘以任意系数值。与偏移一样,虚拟通道可以从用户指定的表中选择一个增益值。

提示

当转换数据的数据长度选择14位/12位/10位时,偏移值的低位将根据数据格式的选择进行切割。

所以,使用常用的12位精度接收数据时,应该注意此处的偏移量为16位,要将偏移量切换成二进制,再统一切换成16位数值。 例如本实验中,电压的偏移值为1.24V,计算得到的偏移量为: 1.24V / 3.3V(参考电压) × 65535 ≈ 24625 ,由于是多的偏置值,要减去, 所以FSP偏移表第一项填写 -24625

随后在虚拟通道中从设定的表中选择一个偏移量,如下图所示:

偏置表选择

这样就省去了我们在软件中的计算量,另外还需要新建一个定时器模块,用来定时打印电压电流值。 有关定时器部分也可以看之前的“瑞萨RA系列定时器详解”章节,这里不再赘述。

6.4.1.4. ADC初始化函数

ADC_Init()函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* @brief 初始化ADC模块
*
* 该函数用于初始化ADC模块,包括打开ADC、配置扫描组和启动扫描组。
* @note 在调用此函数之前,请确保ADC配置结构体已正确设置。
*/
void adc_Init(void)
{
   fsp_err_t err = FSP_SUCCESS; // 定义错误码,初始化为成功状态

   // 初始化ADC模块
   err = R_ADC_B_Open(&g_adc_motor_ctrl, &g_adc_motor_cfg);
   // 检查是否成功打开ADC,失败则断言
   assert(FSP_SUCCESS == err);

   // 配置ADC扫描组
   err = R_ADC_B_ScanCfg(&g_adc_motor_ctrl, &g_adc_motor_scan_cfg);
   // 检查扫描配置是否成功,失败则断言
   assert(FSP_SUCCESS == err);

   // 启动ADC扫描组
   err = R_ADC_B_ScanGroupStart(&g_adc_motor_ctrl, ADC_GROUP_MASK_ALL);
   // 检查扫描组启动是否成功,失败则断言
   assert(FSP_SUCCESS == err);
}

6.4.1.5. 电压电流值读取函数

ADC_GPIO_Config()函数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/**
* @brief 读取ADC电压值
*
* 该函数从ADC通道1读取电压值,并将其转换为实际电压值。
*
* @param[in] 无
* @return 返回读取到的电压值
* @note 此函数在调用前需确保ADC扫描已完成。
*/
double Read_ADC_Voltage_Value(void)
{
   uint16_t adc1_data; // 存储ADC读取到的数据
   double c0; // 存储转换后的电压值

   // 等待扫描完成
   while (!scan_complete_flag)
   {
      ; // 空循环,直到扫描完成标志为真
   }

   // 从ADC通道1读取数据
   R_ADC_B_Read(&g_adc_motor_ctrl, ADC_CHANNEL_1, &adc1_data);

   // 将ADC值转换为电压值
   c0 = GET_ADC_VDC_VAL(adc1_data); // 获取直流电压值
   c0 = GET_VBUS_VAL(c0); // 进一步获取总线电压值

   // 返回电压值
   return c0;
}

/**
* @brief 读取ADC电流值
*
* 该函数从ADC通道3读取电流值,并将其转换为实际电流值。
*
* @param[in] 无
* @return 返回读取到的电流值
* @note 此函数在调用前需确保ADC扫描已完成。
*/
double Read_ADC_Current_Value(void)
{
   uint16_t adc2_data; // 存储ADC读取到的数据
   double c1; // 存储转换后的电流值

   // 等待扫描完成
   while (!scan_complete_flag)
   {
      ; // 空循环,直到扫描完成标志为真
   }

   // 从ADC通道3读取数据
   R_ADC_B_Read(&g_adc_motor_ctrl, ADC_CHANNEL_3, &adc2_data);

   // 计算电流
   c1 = GET_ADC_VDC_VAL(adc2_data); // 获取直流电压值作为电流数据
   c1 = GET_ADC_CURR_VAL(c1); // 进一步获取电流值

   // 置扫描完成标志为假,以便进行下一次扫描
   scan_complete_flag = false;

   // 返回电流值
   return c1;
}

6.4.1.6. 定时打印函数

定时打印
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// GPT中断回调函数
void gpt0_timing_callback(timer_callback_args_t *p_args)
{
   // 定时器溢出事件
   if (TIMER_EVENT_CYCLE_END == p_args->event)
   {
      Time += 5;

      // 读取当前电流值并加入移动平均滤波器
      double current_value = Read_ADC_Current_Value();
      current_sum -= current_samples[current_index]; // 减去旧值
      current_samples[current_index] = current_value; // 更新当前值
      current_sum += current_value; // 加上新值

      // 更新索引
      current_index = (uint16_t)((current_index + 1) % FILTER_WINDOW_SIZE);

      // 计算采样次数
      if (sample_count < FILTER_WINDOW_SIZE)
      {
            sample_count++;
      }

      // 每秒计算一次平均值并打印
      if (Time == 500)
      {
            // 翻转 LED1
            LED1_TOGGLE; // 每秒翻转一次

            // 计算当前平均值
            double current_avg = current_sum / sample_count;
            printf("电压 = %.2fV, 电流 = %.2fmA\r\n", Read_ADC_Voltage_Value(), (current_avg > current_offset) ? (current_avg - current_offset) : 0);

            // 重置时间
            Time = 0;
      }
   }
}

6.4.1.7. hal_entry入口函数

hal_entry()
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
void hal_entry(void)
{
      /* TODO: add your own code here */

      LED_Init();         // LED 初始化
      Debug_UART9_Init(); // SCI4 UART 调试串口初始化

      /* GPT和电机初始化 */
      Motor_GPT_PWM_Init();
      Motor_Control_Init();

      adc_Init();

      printf("这是一个有刷电机基础控制示例\r\n");
      printf("打开串口助手发送以下指令,可控制电机运行状态:\r\n");
      printf("s----------------电机开始旋转\r\n");
      printf("p----------------电机停止旋转\r\n");
      printf("u----------------电机加速旋转[PWM+10%%]\r\n");
      printf("d----------------电机减速旋转[PWM-10%%]\r\n");
      printf("r----------------电机反向旋转\r\n");

      R_BSP_SoftwareDelay(1, BSP_DELAY_UNITS_SECONDS); //延时1秒

      while(1)
         {
         if(motor_start_flag == true) // 如果电机启动标志为真
         {
               motor_start_flag = false; // 将电机启动标志设为假

               Motor_Control_Start();  // 启动电机
               GPT_Timing_Init();      //启动定时器

               printf("电机启动,当前PWM占空比 = %d%%\r\n", motor_pwm_duty);
         }

         if(motor_stop_flag == true) // 如果电机停止标志为真
         {
               motor_stop_flag = false; // 将电机停止标志设为假

               Motor_Control_Stop();   // 停止电机

               R_GPT_Stop(&g_timer_gpt0_ctrl);  //停止定时器


               printf("************电机关闭************\r\n");
         }

         if(motor_speedup_flag == true) // 如果电机加速标志为真
         {
               motor_speedup_flag = false; // 将电机加速标志设为假

               motor_pwm_duty += 10; // 增加电机的PWM占空比
               if (motor_pwm_duty > 100) {// 如果PWM占空比超过100
                  motor_pwm_duty = 100; // 将PWM占空比限制为100
               printf("已到达最大占空比,当前电机PWM占空比 = %d%%\r\n", motor_pwm_duty);
         }else{
               Motor_Control_SetDirAndDuty(motor_dir, (uint8_t)motor_pwm_duty); // 设置电机方向和占空比

               printf("电机加速,新的电机PWM占空比 = %d%%\r\n", motor_pwm_duty); // 打印当前的PWM占空比
         }
         }

         if(motor_slowdown_flag == true) // 如果电机减速标志为真
         {
               motor_slowdown_flag = false; // 将电机减速标志设为假

               motor_pwm_duty -= 10; // 减少电机的PWM占空比
               if (motor_pwm_duty < 0){// 如果PWM占空比小于0
                  motor_pwm_duty = 0; // 将PWM占空比限制为0
                  printf("已到达最小占空比,当前电机PWM占空比 = %d%%\r\n", motor_pwm_duty);
               }else{
               Motor_Control_SetDirAndDuty(motor_dir, (uint8_t)motor_pwm_duty); // 设置电机方向和占空比

               printf("电机减速,新的电机PWM占空比 = %d%%\r\n", motor_pwm_duty); // 打印当前的PWM占空比
               }
         }

         if(motor_reverse_flag == true) // 如果电机反转标志为真
         {
               motor_reverse_flag = false; // 将电机反转标志设为假

               Motor_Control_Reverse();    // 反转电机

               if (motor_dir == 0) {
                  printf("电机方向翻转,当前旋转方向:逆时针\n");
               }
               else {
                  printf("电机方向翻转,当前旋转方向:顺时针\n");
               }
         }

}
#if BSP_TZ_SECURE_BUILD
   /* Enter non-secure code */
   R_BSP_NonSecureEnter();
#endif
}

6.4.2. 下载验证

编译并下载程序后,复位开发板使程序重新运行,打开串口调试助手,连接好电机以及电机驱动板,启动电机,就可以看到开发板采集的电压值和电流值。

读取结果