串口异步收发的实现--蓝牙遥控车的控制

来源:互联网 发布:空耳yaya淘宝 编辑:程序博客网 时间:2024/04/25 10:03

背景

实习时做的一个预研项目的一部分。做完之后收获还是挺多的,虽然收获的前提是被坑的很惨很惨。。。。
代码地址
https://github.com/Wujh1995/Asynchronous-Serial-Read-Write

  • 用串口下发数据,控制机器人底盘履带转动;
  • 同一个串口接收下位机上送的码盘数据,获得机器人实时信息;
  • 上送/下发的数据格式用协议的形式固定;
  • 由于下位机不定时上送数据,下发数据也不定时,所以需要做成异步形式;
  • 基于Linux平台;
  • 用线程的方式实现异步读写;
  • 读取键盘输入调整发送内容(前进/后退/左右转弯等);

代码结构

  • Bluetooth.h :通信协议头文件,规定了通信所用的结构体以及一些固定参数,也定义了一些下位机用的收发函数,但是这些函数对上位机没用,所以我都注释掉了;
  • Bluetooth.c :下位机的通信函数实现;
  • main.cpp :主函数,实现了线程的注册,读线程函数实现,串口发送,读取键盘按键等所有功能,实际上完全可以根据功能拆成几个文件,如果想要模块化的话;

需要注意的地方

Bluetooth.h

#include <stdint.h>
上面这个头文件需要包含进去,因为Linux平台没有STM32的库,而收发的时候又对数据的位数有和严格的要求;
这个头文件里定义了诸如 int16_t 等位宽固定的变量。

#define __PACKED__     __attribute__ ((__packed__))struct bluetooth_frame //16 Bytes{    int16_t start;    int16_t pwm_l;    int16_t pwm_r;    int16_t encoder_l;    int16_t encoder_r;    int16_t  speed_l;    int16_t speed_r;    int16_t end;}__PACKED__;typedef struct bluetooth_frame bluetooth_frame_t;typedef struct bluetooth_frame* p_bluetooth_frame_t;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

上面的 __PACKED__ 用处就是在储存的时候,将不足一个字节的空间留空,也就是字节对齐。
这个用处在这个程序里不是很能体现出来,因为变量长度都是字节的整数倍,但例如有一个6位的变量,没有加 __PACKED__的话,后面的两位就会储存下一个变量的前两位,而我们读取的时候是按字节读取的,也就是说,这样储存,读取的时候就全乱了!最好平时就养成习惯,字节对齐。
另外,要用 typedef 的原因是,在C语言里,定义一个结构体的语法是
struct StructName VariableName;
而我们一般的习惯写法(C++语法)是
StructName VariableName;
所以 typedef struct bluetooth_frame* p_bluetooth_frame_t;的作用就是,用p_bluetooth_frame_t代替bluetooth_frame*,这样我们写代码的时候就可以用C++语法了,也省的每次定义结构体都要多打一个struct

#ifndef __BLUETOOTH_H#define __BLUETOOTH_H#endif
  • 1
  • 2
  • 3
  • 4

我们很多时候都会在头文件里看见类似的语法,
它的用处就是,防止重定义,如果没有这几句,头文件被多次包含之后就会跪。

main.cpp

p_bluetooth_frame_t BT_CMD = (p_bluetooth_frame_t)malloc(sizeof(bluetooth_frame));//p_bluetooth_frame_t));
  • 1

定义指针的时候,需要先分配空间,分配的空间大小应为指针指向的变量大小,如果是数组,应该分配整个数组大小的空间。
如果不分配空间,就会成为野指针,使用的时候就会出现 【段错误(核心已转储)】的错误。这跟C++里需要new是一个道理。

int set_port(int fd,int nbits)

这是配置串口的函数,具体干了什么我也不是很清楚,基本上也就那样配置不会变了,但是有几句是可以配置的。

newtio.c_cc[VTIME] = 0;newtio.c_cc[VMIN] = 1;
  • 1
  • 2

VTIME指的是等待时间,串口都至少会等待这么长时间才返回数据;
VMIN指的是最小数据长度,串口会等待至少有这么多字节数据才会返回;
所以在这里,就是,串口等待至少收到一个字节数据的时候,就会立即返回。

键盘控制相关函数

跟键盘相关的函数有:

void init_keyboard() //初始化键盘void close_keyboard()  //逆初始化/关闭键盘int kbhit()   //键盘码读取函数,返回1为读取成功int readch()  //键盘码分析函数
  • 1
  • 2
  • 3
  • 4

键盘检测的原理大概就是,先初始化键盘,然后不断执行kbhit()检测是否有敲击,如果有,则返回1,并且用全局变量的方式将键盘码传给readch()分析。

在这里碰到过需要输入回车才能被检测到的问题,要解决需要将终端配置成“非加工模式”。代码里已经配置好了。

在main函数里,用while循环检测是否读到键盘敲击事件,实际上如果有别的事要做,可以把键盘检测写成线程的形式,正好我的代码里线程池留了一个位置,模仿一下就能写一个检测键盘的线程。

线程相关函数

跟线程相关的函数有:

int main() //有一些初始化函数写在了main里void thread_create() //创建线程static void *Read_Thread(void*) //线程函数
  • 1
  • 2
  • 3
  • 线程函数不应该有返回值或者实参,返回值必须是 void* ,参数必须是void* ,最好写成静态函数的形式;
  • 在创建线程之前,需要先初始化线程锁,这是实现线程并行的方法,main函数里的pthread_mutex_init(&mut,NULL);就是初始化线程锁,这是一条内核函数,参数mut是自己的定义的变量,类型为pthread_mutex_t
  • thread_create函数的核心其实就是一条内核函数
    pthread_create(&thread[1], NULL, Read_Thread, NULL))
    其中thread的定义是pthread_t thread[2];,参数1就是线程池里分配给该线程的地址,也就是线程号。
    参数3是线程函数的函数地址。
  • 创建线程后,线程就会自动启动,如果想让线程在需要的时候才工作,可以用一个全局变量做判断条件,变量有效时工作,然后在别的地方修改这个变量,用作开关。

读写相关函数

跟读写相关的函数有:

static void MotoCtrl() //用于封装结构体static int Bluetooth_Write_Serial(void* cmd, int length = BLUETOOTH_CMD_LEN) //写串口static void *Read_Thread(void*) //读线程static void Read_Handle(char* pack, int size) //处理函数
  • 1
  • 2
  • 3
  • 4

大致流程如下:
写: 根据键盘收到的数据改变cmd;然后调用MotoCtrl()函数封装结构体,结构体里的数据会根据cmd改变;然后调用Bluetooth_Write_Serial()写串口,完成发送。
读: Read_Thread()读取到数据后,送到Read_Handle()处理,但是因为这里没怎么处理,所以直接在线程里处理算了。实际上应该需要一个专门的处理函数。

串口的数据是“流”,每次收到的数据不一定是一个整的包,会有粘包,残包的问题。这时候需要做缓冲和检包处理,设置一定大小的缓冲区,收到的数据放里面,检测缓冲区里的数据是否够一个包的大小,这个包是否能通过头校验或CRC校验等,检包完成后才能获得一个个完整的包。
检包操作我写在了Read_Handle()里了。大家可以参考一下。


由于串口的底层收发是小端终结形式,也就是每两个字节会反过来传;
例如: 7FAB , 在串口传输的时候,就会变成 AB7F
所以可以发现,在发送函数和接收函数里,都有一大串的移位操作用于抵消这个问题。
另外,用于发送的数组,最好是char*类型的,这样的话每个元素就正好是一个字节,方便控制和处理。可以用指针强制转换的方式,反正指针都是32位的地址嘛,但是强制转换指针的话就需要考虑上面说到的小端终结的问题。


Bluetooth_Write_Serial(bluetooth_tx_buffer,10); //BT_CMDusleep(20000);//sleep(1);Bluetooth_Write_Serial(bluetooth_tx_buffer+10,6);
  • 1
  • 2
  • 3
  • 4

这里将一个 16 Byte 的数据拆成 10Byte 和 6Byte,分两次发,原因是,下位机是STM32,串口接收缓冲区貌似只有10个Byte,如果一次性发送16Byte,第11Byte就会丢失;
两次发送之间需要短暂的延时,因为没有延时的话,其实就相当于一次性发送;usleep里的参数单位是us,这里是延时了20ms;

写在后面

其实讲道理上面搞了这么多,就是搞了个蓝牙遥控车的控制而已,需要学习的东西还很多,Stay humble,Stay hungry,Keep walking~

原创粉丝点击