电动滑板开发日志owo

材料准备

*每个原件说明后的括号内为其大致价格,价格低于1元的以1元表示

  1. 控制板

    Arduino Pro Mini:主控板,用于协调控制板上的各个器件,其作用有接受发送蓝牙遥控信号、转换电源电压、测量电池电压、测量霍尔元件电位、输出PWM控制信号到电调(20¥)

    HC05蓝牙模块:用于处理与遥控器的通信(20¥)

    霍尔元件及磁石:用于测量从动轮转速(15¥)

    按压开关:用于控制PWM信号的输出(1¥)

    电阻:用于将6节锂电池的24V电压分压供主控板测量(1¥)

    12*18PCB电路板:放置各个原件,虽然最后懒得往上面焊了(15¥)

  2. 遥控器

    Arduino Pro Mini:主控板,用于处理遥控器的相关信号,其作用有接受发送蓝牙控制信号、接受转换摇杆信号、转换电源电压、输出I2C信号到显示屏(20¥)

    HC05蓝牙模块:用于处理与控制板的通信(20¥)

    摇杆模块:用于控制速度与刹车(2¥)

    SSD1306显示屏:用于显示实时电量、速度、动力大小(20¥)

    8*2PCB电路板:放置各个原件(2¥)

    电源:南孚牌9V锌锰干电池(暗示广告费)(20¥)

  3. 其他主要元件

    长板滑板:本体(110¥)

    滑板海鸥架:用于控制滑板转向(2 * 60¥)

    锂电池:用于给电机提供动力,这里选用了5200mAh的6C锂电池(285¥)

    9052轮:轮越大越稳,就买了大的(4 * 40¥)

    N5065 330KV 电机:控制轮子转向等(180¥)

    120A电调:用于控制输出到电机的电流(220¥ )

    电机支架、主动轮、皮带等:用于固定、连接各个部件(50¥)

  4. 配件

    B6充电器:真正意义上的万能充,用来平衡锂电池充电(110¥)

    CP2102 TTL转USB转接头:用于给arduino烧录程序,需要手动reset,不太好用,换用了下面这款(5¥)

    FIDI下载器:用于给arduino烧录程序(40¥)

    各种杜邦线:连接用,买长了,其实5cm就够用了(20¥)

    电调编程器:设置电调参数(35¥)

    3M胶带:绝缘与固定(5¥)

    热缩管:绝缘与固定(5¥)

    LED灯:显示状态与照明(5¥)

    树莓派3B+:本来打算做一个树莓派project,但是感觉有些水,就把树莓派用来烧录arduino BootLoader了

    电烙铁、热熔胶枪、万用表、钳子、螺丝刀等:杂七杂八的常用工具不细说了

总花费大约1k出头,考虑到同等的速度、功能、续航、可扩展性等因素,比市面上的电动滑板还是要便宜很多的,下面就正式开始介绍开发过程啦

一、动力系统

  由于很多元件没有统一标准,需要自行设计很多用来连接的部件,过于麻烦,所以这一部分是找店家整体定做的,如果自行做图纸设计找厂家出模具的话很多部分甚至能便宜一半的价格,但是这毕竟是程序设计课… 不需要那么多过于硬核的设计工作…

  使用了N5065的无刷电机,50代表其外径,65代表其长度,这里使用了330KV的版本,也就意味着电压每升高1V,转速就会升高330。同时电机的输入电流是三相的,避免了电刷的使用,减小了摩擦力,提高了工作效率,但这也就意味着需要使用电调来控制了。电调的全程是电子调速器,它可以把电池输出的直流电在三根电线中循环输出,控制电机转动,并且可以随时改变他们的电流值,以达到控制速度的效果,电调为主控板提供了BEC接口,主控板㛑可以通过这个接口给电调和电机输出PWM信号。

120A电调

N5065电机

  PWM信号的全称是脉冲宽度调制(Pulse-width modulation),不同于普通的数字信号和模拟信号,PWM是一段波动的数字信号,它只有1和0两种状态,但是它可以用一段时间内1的时间占比来表示实际数值,比如在一段20ms的时间内,想要表示0.5即可让1输出10ms,在实际使用的时候我们需要把遥控器传来的力度大小转换为PWM信号输出给电调,代码如下:

1
2
3
4
5
#define MOTOR_BASE 1100
#define MOTOR_COEF 8

PWMTime =
(double)MOTOR_BASE + (double)power * (double)MOTOR_COEF;

  然后使其在周期内使用Arduino自带的随动系统Servo进行输出即可

1
2
3
4
5
6
7
8
#define MOTOR_PIN 12

motorServo.attach(MOTOR_PIN);

if (nowTime - lastContral >= CONTRAL_INTE) {
lastContral = nowTime;
motorServo.writeMicroseconds(PWMTime);
}

二、通信系统

遥控器上的蓝牙模块

主控板上的蓝牙模块

  由于WLAN价格昂贵,ZigBee资料太少,这里最终选择了蓝牙模块进行通信,首先我们需要通过串口转USB的转换器连接蓝牙,进行蓝牙相关信息的设置,可以参考以下步骤:

  1. EN置高,通电进入AT模式
  2. 通过串口38400波特率,8位数据位,1位停止位进行命令传输
  3. 通过串口发送AT\n进行连接测试,返回OK即连接成功(注意一定要加上换行符)
  4. 主从机发送AT+NAME=<Param>\n给它们设置一个可爱的名字/w\
  5. 主机发送AT+ROLE=1\n,从机发送AT+ROLE=0\n设置主从模式
  6. 主从机发送AT+PSWD=<Param>\n设置配对码,方便调试
  7. 从机发送AT+ADDR?\n查询mac地址,记为MACA
  8. 主机发送AT+BIND=<MACA>\n来绑定从机,这里的MACA为上一步返回值
  9. 主从机默认波特率应该为9600,这意味着1s内只能传输9600bit即1200Byte的数据,1ms大约只有1Byte的数据传输,这个速度不太行,所以主从机发送AT+UART=38400,0,0\n来重置波特率

  至此,蓝牙连接完毕,将蓝牙连接到arduino只需要将串口连接,即进行RX-TX, TX-RX的连接即可,在程序内采用如下方式打开串口并进行数据的接收与发送

1
2
3
4
5
6
7
Serial.begin(38400);

if (Serial.available() > 0) {
char tmp = Serial.read();
}

Serial.print("test");

  由于蓝牙模块的双工异步处理,可能会导致发送端与接收端统一频率下接发数据产生延迟而造成数据丢失或者累积错位,这也就导致了遥控器发出信号主控板无法抄收、主控板抄收延迟等问题,所以我们必须规定一定的数据格式,或者高端一些叫做通信协议,我在进行设计时主要遵循以下两个原则:

  1. 定义B字符为起始符,E字符为结束符,只以起始符作为读取的开始,一旦遇到结束符立马停止读取

  2. 定义数据包大小为定值,即在B和E中间的数据必须为指定长度的字符,否则数据包作废

  这样设计通过增加数据的复杂性来提升数据的安全性,但是仍会遇见一些问题,初步分析是由于在设计之初没有考虑到蓝牙的连接时长,即默认蓝牙一旦开机就会连接,并且开机后双方均以相同的频率收发数据,如每100ms发送一次,每100ms接受一次,这就很容易导致发送端的数据在接收端缓冲区积累,造成数据延迟,对于这个问题有两个解决办法,一是每次只取缓冲区内最新数据读取,丢弃冗余数据,二是消除接收端频率设计,使接收端保持接收,即发送端每100ms发送一次,接收端每时每刻都在接收并且随时覆盖旧的数据,每100ms进行一次更新,这两种解决办法本质相同,二更安全也更容易实现一些,最终蓝牙传输代码如下:

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
/* ---- 发送端 ---- */
Serial.print("B");
Serial.print(power); // if power < 10 then power = 10 保证数据长度
Serial.print("E");

/* ---- 接收端 ---- */
int lossCount;
if (Serial.available() >= 4) {
char tmp = Serial.read();
delayMicroseconds(FLUSH_INTE);
String powerString;

if (tmp == 'B') {
while (Serial.available() > 0) {
tmp = Serial.read();
if (not ISNUM(tmp)) {
if (tmp != 'E') powerString = "";
break;
}
powerString += tmp;
delayMicroseconds(FLUSH_INTE);
}

if (powerString.length() > 0) {
int power = getNumber(powerString);
if (power > 100 or power <= 0) power = 50;
PWMTime =
(double)MOTOR_BASE + (double)power * (double)MOTOR_COEF;
} else {
lossCount++;
if (lossCount >= 5) {
PWMTime = 1500;
lossCount = 0;
}
}
}
}

三、控制系统

  采用摇杆进行控制,前推是前进,不同的位置对应着不同的速度,后拉是紧急刹车,中位是滑行摇杆

  摇杆有VCC、GND、VRx、VRy、SW五个接口,由于我们只需要一个方向的输出,所以只需要接VCC、GND、VRx即可,其中VRx可以连接任意一个模拟输入口,并通过以下方式读取输入

1
2
3
#define REMOTE_PIN A0

int sensorV = analogRead(REMOTE_PIN);

  这样读入的是一个int类型的变量,其值为0到1023,通过以下代码可以转换为0V到5V的电压输出

1
2
double V = (double)sensorV * 5.0 / 1023.0;
int power = (double)sensorV * 100.0 / 1023.0 + 0.5; // 或直接这样转换为一个0到100的动力大小输出

四、测速系统

霍尔元件及其磁石

  测速采用霍尔元件,根据霍尔原理,当其感应器附近有磁场通过时其原件内部会产生一定大小的电势差,装在从动轮上的磁石每经过海鸥架上霍尔元件一次,霍尔元件就会将这样的电势差通过AD转换器转换为数字信号,传给主控板,主控板通过如下方式进行速度的测量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#define HOLL_PIN 10
#define WHEEL_DIAM 9.0

/* setup */
pinMode(HOLL_PIN, INPUT);

/* main loop */
int hollSta = digitalRead(HOLL_PIN);
if (hollSta != lastHollSta) {
hollCount++;
lastHollSta = hollSta;
}

/* upload loop */
unsigned long rps = hollCount * UPLOAD_FREQ / 2;
hollCount = 0;
unsigned long velocity = PI * WHEEL_DIAM * (double)rps / 100.0;

Serial.print("BV"); // speed
sprintf(output, "%03d", velocity);
Serial.print(output);
Serial.print("E");

五、电源系统

电压传感模块

  遥控器使用干电池连接RAW口供电,控制板使用锂电池的BEC接口供电,干电池是一次性的,耗尽后换新即可,而锂电池一旦过耗,可能会减少电池寿命,所以需要实时监测锂电池电压,在截止电压时及时切断电路,防止锂电池过耗。这里采用了R1(3kΩ)与R2(750Ω)的电阻串联来达到对24v+电压的分压作用,分压后便可直接将R2两段电压输出到主控器的模拟输入口经过计算即可获得电路实际电压,这里的分压比是1:4,但是考虑到锂电池满电电压是4.2V,即6C锂电池最大可能达到25V+,为了防止烧坏电路,我又在R2上并联了3kΩ的电阻R3,则R2、R3的阻值为$\frac{1}{\frac{1}{3000} + \frac{1}{750}}=600(Ω)$,分压比变为了1:5,就可以放心使用啦,程序里面只需要读取模拟输入口数据即可测量电压

5200mAh 6C锂电池

1
2
3
4
5
6
7
#define BAT_PIN A0

int batSensorV = analogRead(BAT_PIN);
double batV = batSensorV * (5.0 / 1023.0);
aveBatV = (double)batCount / (double)(batCount + 1) * aveBatV +
batV / (double)(batCount + 1); // 这里为了防止溢出牺牲了一部分精度
batCount++;

  但是!锂电池的电压并不与电量成正比,经过单位时间相同功率的电能耗费实验,大概可以得出锂电池电压与电源的对应关系,于是就可以通过如下函数预估当前的大概电量啦ovo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int voltageToPersent(double v) {
if (v >= 4.17) {
return 100;
} else if (v >= 3.8) {
return 60 + (int)((v - 3.8) / 0.4 * 40.0 + 0.5);
} else if (v >= 3.1) {
int res = ((v - 3.1) / 1.1 * 100 + 0.5) - 4;
return res > 0 ? res : 0;
} else {
return 0;
}
}

Serial.print("BP"); // power
sprintf(output, "%03d", voltageToPersent(aveBatV));
Serial.print(output);
Serial.print("E");

六、时序控制

  基本沿袭了arduino本身的中断控制等程序,除此之外还调用了micros()函数获取自程序运行到现在的时间戳进行时间的计算,这里需要注意由于arduino开发板的问题,在一些系统内int类型是16位而非32位的,需要使用unsigned long来存储时间变量。

  我采用了如下方式进行时间控制

1
2
unsigned long nowTime = micros();
if (nowTime - lastContral >= CONTRAL_INTE)

  我们定义上传时间间隔为100ms(100000μs),PWM信号时间间隔为2ms(2000μs),串口缓冲区刷新即数据传输间隔为0.3ms(300μs),绘图时间间隔为1000ms(1000000μs)

1
2
3
4
5
6
#define CONTRAL_INTE 2000
#define FLUSH_INTE 300
#define UPLOAD_INTE 100000
#define UPLOAD_FREQ 10
#define DELTA 500
#define DRAW_INTE 1000000

七、安全系统

  由于该产品面向实际使用,所以需要格外注意安全问题,比如蓝牙模块失联时不能继续发送信号、蓝牙发送数据错乱时跳过数据、主控程序失控时有外部方式中断电机等,其中有一部分安全问题由电调解决,如电调设定温控模块、电流保护模块等,其他问题在代码层面主要采用了如下解决方案:

  1. 蓝牙失联或数据无法正常接受数据超过5次即进入滑行模式(PWM时间1.5ms为滑行模式)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    if (powerString.length() > 0) {
    int power = getNumber(powerString);
    if (power > 100 or power <= 0) power = 50;
    PWMTime =
    (double)MOTOR_BASE + (double)power * (double)MOTOR_COEF;
    } else {
    lossCount++;
    if (lossCount >= 5) {
    PWMTime = 1500;
    lossCount = 0;
    }
    }
  2. 蓝牙失联屏幕进行提示并终止循环程序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #define BLUETOOTH_STATE 10

    if (digitalRead(BLUETOOTH_STATE)) {
    noLink = false;
    noLinkDraw = false;
    } else {
    noLink = true;
    }

    if (noLink) {
    if (not noLinkDraw) {
    drawNoLink();
    noLinkDraw = true;
    }
    return;
    }
  3. 主控板状态异常(如测速异常、电压传感异常等)进行屏幕提示

    1
    2
    3
    if (uploadCount == 0 or speedCount == 0 or batteryCount == 0) {
    drawError();
    }
  4. 外部按钮控制:

  在主控板到电调的PWM信号上加入了一个按压式开关,放在了正面前脚掌处,只有在脚踩时PWM信号才会正常传输

一个不怎么好用的按钮

八、显示系统

  使用了SSD1306模块0.96寸的OLED屏幕作为显示屏,将arduino A5与A4两个口置为SDA和SCK输入口,通过I2C方式与显示屏进行数据传输,代码实现上直接调用了u8g2库,使用了其HWI2C接口,主要实现了速度、动力、电量显示与异常提醒等函数,考虑到arduino的处理性能和绘图能力嘛emmmm… 直接每秒一帧了… 后期考虑加入另外一块arduino作为绘图卡专门用来处理显示数据

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
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(
U8G2_R0, U8X8_PIN_NONE, A5,
A4);

u8g2.begin();

void drawError() {
u8g2.setFont(u8g2_font_helvB18_tr);
u8g2.clearBuffer();
u8g2.drawStr(8, 64, "ERROR!");
u8g2.sendBuffer();
}

void drawData(int capacity, int velocity, int power) {
u8g2.setFont(u8g2_font_helvB10_tr);
u8g2.clearBuffer();
char output[32];

sprintf(output, "BTR:");
u8g2.drawStr(8, 16, output);

sprintf(output, "SPD:");
u8g2.drawStr(8, 48, output);

sprintf(output, "PWR");
u8g2.drawStr(70, 16, output);

u8g2.setFont(u8g2_font_helvR10_tr);

sprintf(output, "%d%%", capacity);
u8g2.drawStr(8, 32, output);

sprintf(output, "%dm/s", velocity);
u8g2.drawStr(8, 64, output);

if (power >= 88) {
u8g2.drawBox(65, 20, 45, 10);
} else {
u8g2.drawFrame(65, 20, 45, 10);
}

if (power >= 75) {
u8g2.drawBox(65, 30, 45, 10);
} else {
u8g2.drawFrame(65, 30, 45, 10);
}

if (power >= 63) {
u8g2.drawBox(65, 40, 45, 10);
} else {
u8g2.drawFrame(65, 40, 45, 10);
}

if (power > 50) {
u8g2.drawBox(65, 50, 45, 10);
} else {
u8g2.drawFrame(65, 50, 45, 10);
}

u8g2.sendBuffer();
}

void drawNoLink() {
u8g2.clearBuffer();
u8g2.setFont(u8g2_font_helvB18_tr);
u8g2.drawStr(8, 64, "NOLINK!");
u8g2.sendBuffer();
}

九、成品展示

  这里放不了GIF和视频,视频就直接加在附件了,电路图是手绘的,比较简单,在这里就不给出了

遥控器正面

遥控器侧面

主控板

十、其他

  这里主要用来放一些收获不足和想吐槽的话…

  1. 这次project收获挺多的,算是对电路原理有了初步的了解,也产生了浓厚的兴趣,下一步想做一个微型无人机集群,但是考虑到成本问题嘛.. (室内定位用的UWB模块一个50左右,基站2000左右,实在是有些昂贵)可能要咕一咕了
  2. 最主要的收获主要还是硬件层面的一些接口、协议,比如蓝牙如何设置,串口如何使用、GPIO引脚的作用、数字信号模拟信号PWM信号的发送方式、各种模块的工作原理及其作用等等等等,其次就是代码上对于单线程的单片机,使用中断或集群等操作来达到多线程的目的,以及各种各样的时间处理方式、优美减少BUG的编码方式之类的
  3. 不足的地方大概是布线太硬核(丑)了,以及最开始买东西的时候没有和商家充分沟通,买来了一些不能用的东西,还有就是一些底层设计上优化不到位(为了遥控器的显示几乎占用了arduino pro mini的所有动态内存,这是相当危险的!)
  4. 其实本来是想做一个树莓派人脸识别门禁的project,但是大家都在人脸识别,恰好看到生命取向的同学用滑板非常酷,于是在已经完成了那个门禁系统的前提下,中途易辙做了电动滑板,而门禁用的磁吸磁石… 就被我用来当霍尔元件的磁石了哈哈哈

  5. 这个CP2102是分6pin和4pin的,我买的是4pin的版本… 他自己不带DTR模块导致需要手动按arduino的reset键,关键是这个时机很难把握,然后在youtube上看到了毛子的神奇解决方式… 大概是把芯片组第三号引脚用细铜丝引到RST针脚上… 技不如人,告辞

毛子牛逼!

特别鸣谢:感谢万能的老爹用他强大的知识储备耐心帮忙解答了各种各样的问题,以及感谢辰辰小朋友全程陪同及参与测试2333