首发:Rice 嵌入式开发技术分享
作者:RiceDIY
简要
- 平衡车文章分为4篇进行说明:
- 《平衡车 - 硬件》:讲解平衡车的硬件设计。
- 《平衡车 - 软件》:讲解平衡车的软件设计,算法。
- 《平衡车 - 上位机》:讲解调参上位机的设计
- 《平衡车 - 微信小程序》:讲解微信小程序作为遥控器的实现。
软件设计
代码结构
- 平衡车的代码设计,该平衡车是基于RT-THREAD NANO上进行设计,主要分为3层,driver-device-controler。
- driver层:主要对接STM32的HAL层。这部分的代码,模仿了rt-thread完整版的设备驱动框架。
- device层:主要实现平衡车各种外设的驱动。
- controler层:主要实现平衡车的算法,控制,显示,通信等功能。
driver层:
- 主要包含如下驱动:| 驱动 | 功能 | |------|------| | drv\_adc | 测量电压,提供原始数据 | | drv\_flash | 保存平衡车参数,提供操作flash接口 | | drv\_gpio | 通用GPIO的,效仿RTT完整版接口 | | drv\_pulse\_encoder | 脉冲解码器,提供读取编码器数值 | | drv\_pwm | 提供电机驱动底层接口 | | drv\_soft\_i2c | 提供软件I2C接口,可任意扩展 | | drv\_uart | 驱动串口接口,可任意扩展 |
- 以上的驱动都是根据rt-thread完整版的思想,进行简化,为上层提供统一的接口。
- 以采集ADC 驱动为例:
- 将名字和ADC1实例绑定:
#ifdef RT_USING_ADC1
#define ADC1_CONFIG \
{ \
.name = "adc1", \
.Instance = ADC1, \
}
#endif
#ifdef RT_USING_ADC2
#define ADC2_CONFIG \
{ \
.name = "adc2", \
.Instance = ADC2, \
}
#endif
- 为上层提供统一的API,通过rt\_find\_adc()接口找到相对应的实例句柄,然后通过rt\_adc\_enabled()接口使能对应的实例,通过rt\_get\_adc\_value()接口读取对应的通道的ADC值。
struct rt_adc_drv *rt_find_adc(char *name);
rt_err_t rt_adc_enabled(struct rt_adc_drv *obj, rt_bool_t enabled);
rt_err_t rt_get_adc_value(struct rt_adc_drv *obj, rt_uint32_t channel, rt_uint32_t *value);
device层:
- device层主要提供传感器的原始数据,设备控制接口。设备驱动如下:| 设备 | 功能 | |------|------| | dev\_ble | 提供BLE的发送与接收接口,对接drv\_uart | | dev\_buzzer | 提供控制蜂鸣器接口,对接drv\_gpio | | dev\_encoder | 提供读取编码器数值接口,对接drv\_pulse\_encoder | | dev\_key | 提供读取按键值接口,对接drv\_gpio | | dev\_motor | 通过控制电机接口,对接drv\_pwm | | dev\_mpu6050 | 提供陀螺仪读取数值接口,对接drv\_soft\_i2c | | dev\_oled | 提供陀控制oled接口,对接drv\_soft\_i2c | | dev\_voltage | 通过读取电压接口,对接drv\_adc |
- 以采集ADC 驱动为例:
- 获取对应实例的句柄,以及使能对应实例:
int rt_voltage_dev_init(void)
{
voltage_adc_drv = rt_find_adc("adc1");
if(voltage_adc_drv == RT_NULL)
{
rt_kprintf("find adc1 fail\n");
return RT_ERROR;
}
rt_adc_enabled(voltage_adc_drv, RT_TRUE);
return RT_EOK;
}
- 获取电压, 上层只需要调用此接口,即可完成电压读取:
rt_uint32_t rt_get_voltage(void)
{
rt_uint32_t vol = 0;
rt_uint32_t value = 0;
rt_get_adc_value(voltage_adc_drv, VOLTAGE_ADC_CHANNEL, &value);
vol = (value * REFER_VOLTAGE / CONVERT_BITS) * 11;
rt_kprintf("the voltage is :%d.%02d \n", vol / 100, vol % 100);
return value;
}
controler层:
- ble\_ctrl: 提供BLE与上位机/微信小程序的控制逻辑
- 串口一个数据发送线程。
- 注册BLE接受数据回调函数,接受上位机/微信小程序的控制逻辑。因为采用中断以及为了分层,所以采用回调的形式。
- show\_menu: oled显示,参数设置,参数显示等控制逻辑
- 初始化启动了一个线程,用于运行时参数数据实时显示。
- 每次重新启动都会进入此功能,通过按键和oled,可进行PID,速度等参数整定。
- sds:这是一款虚拟示波器,通过串口输入,在我这个平衡车中,我采用BLE转发
- 方便在整定参数的时候使用。
- controler:控制层的总入口,角度计算,平衡PID算法,速度计算,速度PID,方向PID。
- 创建一个线程,然后每运行一次延时5ms。线程执行内容:
static void ctrl_thread_entry(void *parameter)
{
for(;;)
{
ctrl_get_angle(); //获取角度
ctrl_balance_pid(); //平衡PID计算
ctrl_get_speed(); //获取当前速度
ctrl_speed_pid(); //速度PID计算
ctrl_turn_pid(); //转向PID计算
ctrl_set_speed(); //控制速度
rt_thread_delay(5);
}
}
- 角度计算,我直接采用三角函数进行计算,没有采用四元数:
turn_parm.turn_gyor = gyro.z / 16.384;
angle_parm.balance_gyor = gyro.y / 16.384;
angle_parm.angle = atan2(accel.x, accel.z) * 180 / PI;
angle_parm.angle_increment = angle_parm.out_angle - (gyro.y / 16.384) * 0.005;
angle_parm.out_angle = K1 * angle_parm.angle + (1 - K1)*(angle_parm.angle_increment);
- 平衡PID算法,采用PD控制算法:
int ctrl_balance_pid(void)
{
float bias_val = 0.0;
bias_val = (car_parm.blc_angle - 0.5) - angle_parm.out_angle;
speed_parm.balance_pwm = car_parm.blc_Kp * bias_val + car_parm.blc_Kd * angle_parm.balance_gyor;
return speed_parm.balance_pwm;
}
- 速度计算,直接获取左右编码器的值
- 速度PID,采用PI控制算法:
void ctrl_speed_pid(void)
{
float current_bias = 0;
current_bias = (speed_parm.get_left_speed + speed_parm.get_right_speed) - car_parm.speed;
current_bias = speed_parm.last_bias * 0.3 + current_bias * 0.7;
speed_parm.integral_bias += current_bias;
speed_parm.speed_pwm = (int)(car_parm.speed_Kp * current_bias +
car_parm.speed_Ki * speed_parm.integral_bias);
speed_parm.last_bias = current_bias;
}
- 速度PID,我这里是没有太关乎的,只有P值.
- 速度设置,将平衡PID、速度PID、转向PID计算出来的整合既是最终速度的值。
效果:
关注微信公众号『Rice嵌入式开发技术分享』,后台回复“微信”添加作者微信,备注”入群“,便可邀请进入技术交流群。
推荐阅读
更多嵌入式技术干货请关注Rice 嵌入式开发技术分享