# GlobalLocalization **Repository Path**: WangXi_Chn/GlobalLocalization ## Basic Information - **Project Name**: GlobalLocalization - **Description**: Albatross(信天翁):机器人平面全方位定位系统设计代码仓库 - **Primary Language**: C - **License**: BSD-3-Clause - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 0 - **Created**: 2020-09-05 - **Last Updated**: 2021-12-15 ## Categories & Tags **Categories**: Uncategorized **Tags**: RT-Thread ## README # 【RT-Thread】平面全方位定位系统应用程序 # 程序迭代记录 ## Gitee [https://gitee.com/WangXi_Chn/GlobalLocalization](https://gitee.com/WangXi_Chn/GlobalLocalization) ## 应用版本V1.0 ### 应用功能 - 可与上位机通信(遵从板级串口通信模块的通信协议) - 支持与其他STM32单片机通信(遵从板级串口通信模块的通信协议) - 支持JC24B无线串口透传模块 - 支持LED频闪显示自身ID - 支持陀螺仪芯片MPU9250驱动,计算获取偏航角 - 支持Flash外置存储芯片驱动,文件操作系统 ### 开发环境 - CubeMX - MDK5 IDE - STM32F405芯片 - RT-Thread操作系统 - 面向对象模块接口设计 - JC24B无线串口 - MPU9250 - W25Q256芯片 ### 应用特性 - 模块化设计自由裁剪 - 多线程工作 - 通信模块通用,分自身ID和目标ID,可迅速部署通信网络 - 数据表大小0xFF个int类型数据 - 内部集成Kalman滤波算法模块 ### API说明 - 同[https://www.yuque.com/wangxi_chn/qaxke0/licn24#lWgZq](https://www.yuque.com/wangxi_chn/qaxke0/licn24#lWgZq)类似 - 可详细参考代码仓库中源文件 ### 补充说明 - 项目基于硬件版本2.0 - [https://hitwhlc.yuque.com/hero-rc/zlndvo/sixk4b](https://hitwhlc.yuque.com/hero-rc/zlndvo/sixk4b) - 此应用程序为该系统核心功能 - 目前陀螺仪测角功能正常,与其他部署相同板级通信系统的通信功能正常 - 测角准确度还需配合测试装置调整 - 目前尚未开发全部功能,等待后续扩展开发 ## 应用版本V2.0 ### 应用更新 - 赋予项目名称 **_Albatross(信天翁)_** - 寓意长时间续航能力下定位准确 - 支持通过无线串口发送信息至上位机、单片机等(遵从板级串口通信模块的通信协议) - 已通过MiniSpider项目中的OLED显示测试数据(通信链路测试) - 磁编码器里程计功能实现 - 与陀螺仪数据融合得到定位数据初步功能实现(准确度待验证) - 支持通过RTT Finsh命令行修改自身ID(重新上电会重置为默认ID 0x01) - 支持通过RTT Finsh命令行给指定ID的设备发送帧(需要支持板级串口通信模块的通信协议) ### 补充说明 - JC24B无线串口天线非全局定位原设计功能,对其中的串口接线修改适配功能 - 测试过程中发现问题,磁编码器在仅ST下载器供电时无法读取数据,需要串口供电才可 - 猜测此编码器的供电引脚与下载器电源接线部分存在问题 ## 应用版本V3.0 ### 应用更新 - 因信道碰撞严重,放弃使用JC24B无线串口天线 - 修改无线串口天线为AS13-TTL - 已与PC端上位机通信成功 - 适配全局定位测试装置,放弃与测试控制台的通信,现只可与PC端上位机通信 ### 补充说明 - 项目移交 - 现总结项目结构和程序说明 # 应用程序结构 ![image.png](https://cdn.nlark.com/yuque/0/2021/jpeg/427268/1609752859568-82ab9e50-0b5d-45a3-a79e-836329e3fc4a.jpeg) ## 类 ### 模块类 - 所有的模块器件都被抽象成为类,使用C语言的结构体实现 - 结构体中有成员变量,有成员函数 - 与应用程序处于不同的代码仓库下,支持单独更新升级 - [https://gitee.com/WangXi_Chn/RttOs_ModuleLib](https://gitee.com/WangXi_Chn/RttOs_ModuleLib) - 以LED模块为例 - 每个模块类的定义都在.h文件中 ```c struct _MODULE_LED { /* Property */ rt_base_t Property_pin; /*!< Specifies the LED module pins to be configured. This parameter is defined by function @ref GET_PIN(GPIOPORT, GPIO_PIN_NUM) */ enum LED_MODE Property_Mode; rt_uint32_t LED_TIME_CYCLE; /* if Property_Mode is FLASH_LED_MODE, must be defined*/ rt_uint32_t LED_TIME_OUTPUT; /* Value */ rt_uint16_t last_time_show_cycle; // 状态查询时间 rt_uint16_t set_time_cycle; // 循环时间 rt_uint16_t set_last_time; // 上一次电平输出时间 rt_uint8_t curr_number; // 当前 rt_uint8_t next_number; // 下一个指示次数 /* Method */ void (*Method_Init)(struct _MODULE_LED *module); void (*Method_Handle)(struct _MODULE_LED *module); void (*Method_Set)(struct _MODULE_LED *module,rt_uint8_t number); }; typedef struct _MODULE_LED MODULE_LED; ``` - 注释/* Property */下的,为使用该结构体声明一个变量时(即类的例化),必须给出的初始值(即类的属性) - 一般为与硬件或RTT的对应关系 - 注释/* Value */下的,为一些变量,不必赋予初值,会在程序运行中发生变化 - 一般是一些中间计算结果,或者是输出结果 - 可用来观察,调用,调试 - 注释/* Method */下的,为成员函数(即类的方法),该模块提供的一些程序段 - 使用 . 成员函数 即可实现对其的调用 - 所用的模块都对成员函数有固定含义的命名 - Method_Init 一般为对该模块的初始化,用户一般不直接使用该方法 - (在该模块的全局函数Config中自行运行) - Method_Handle 一般为该模块的中断服务函数,用户一般需要将其放到一个线程中,会自行阻塞运行 - Method_Set 一般为该模块的参数设置,可根据模块功能和参数组成决定 - Method_Get 一般为获取该模块的某个数值 **注意:由于这里是使用了函数指针作为结构体成员,代替实现了类似于方法的功能,但是还需要将实际的函数与函数指针完成映射,映射过程是在每个模块的全局函数** **Module_XXX_Config 中实现的,同时模块的 .Init 方法也在这里被调用** ```c /* Global Method */ rt_err_t Module_Led_Config(MODULE_LED *Dev_LED){ if(Dev_LED->Method_Init==NULL && Dev_LED->Method_Set==NULL && Dev_LED->Method_Handle==NULL ){ /* Link the Method */ Dev_LED->Method_Init = Module_LedInit; Dev_LED->Method_Set = Module_LedSet; Dev_LED->Method_Handle = Module_LedHandle; } else{ rt_kprintf("Warning: Module Led is Configed twice\n"); return RT_ERROR; } /* Device Init */ Dev_LED->Method_Init(Dev_LED); return RT_EOK; } ``` **因此在使用各个模块前必须调用该函数,实现对模块的初始化,否则会在RTT中报堆栈溢出的错误** ### 应用类 - 将整个应用程序也抽象成为了一个类 _APP_GLOBALPOS - 类的成员即为各个模块,实现类的嵌套(结构体嵌套) - 类的方法只有两个 - Method_Init 应用程序初始化(不由用户调用,Config自行完成) - Method_Run 应用程序运行 - 这两个方法仅在main中被调用,作为应用程序的入口 - 对该应用类的例化(结构体变量声明)也在main中进行,成为该应用程序几乎唯一的全局变量 ![image.png](https://cdn.nlark.com/yuque/0/2021/png/427268/1609747335330-302dc3af-4c9f-4a50-9c98-4fecde7c73d3.png#align=left&display=inline&height=254&margin=%5Bobject%20Object%5D&name=image.png&originHeight=600&originWidth=803&size=168797&status=done&style=none&width=340) - 在Debug中,将此变量添加在监视窗口,可观察所有模块的运行状态 - 应用类的全局方法 APP_XXX_Config - 应用类成员函数的绑定 - 应用类所有模块的Config ```c void APP_GlobalPos_Config(APP_GLOBALPOS *Application) { if( Application->Method_Init == NULL && Application->Method_Run == NULL ){ /* Link the Method */ Application->Method_Init = APP_GlobalPosInit; Application->Method_Run = APP_GlobalPosRun; } else{ rt_kprintf("Warning: Module Led is Configed twice\n"); return; } /* Device Init */ Application->Method_Init(Application); /* Module Config */ Module_Led_Config(&(Application->dev_Led)); Module_File_Config(&(Application->dev_SpiFile)); Module_UartCom_Config(&(Application->dev_UartBsp)); Module_MPU9250_Config(&(Application->dev_Mpu9250)); Module_AS5048_Config(&(Application->dev_As5048_left)); Module_AS5048_Config(&(Application->dev_As5048_right)); return; } ``` - 在应用类的Init方法中,实现对其所有模块的/* Property */变量的赋值 ```c static void APP_GlobalPosInit(APP_GLOBALPOS *Application) { /* Module param list ------------------------------------------------------------------------- */ /* LED device */ /* Pin: PA10 Low power enable */ Application->dev_Led.Property_pin = GET_PIN(A, 10); Application->dev_Led.Property_Mode = FLASH_LED_MODE; Application->dev_Led.LED_TIME_CYCLE = 1500; Application->dev_Led.LED_TIME_OUTPUT = 150; ...... } ``` ## 线程 ### 基本组成 - 应用类的方法中 Method_Run 是个一次性方法,其作用是 - 创建线程 - 创建调度线程的信号量等 - 所有的应用程序根据实际需要基本上都有这样几个线程 - **XXXLed_thread** - 系统状态灯线程,从其频闪中可以传达一定信息,而且可观察RTT是否运行正常 - **XXXUpdate_thread** - 数据更新线程,一般是处于阻塞态线程,等待串口、CAN等缓存区释放信号量 - **XXXDeal_thread** - 数据处理线程,处理应用程序中的数据运算 - **XXXShow_thread** - 显示线程,一般外界OLED、串口屏等,发送显示信息 - 各个模块相互作用的机会仅存在于应用线程这一层,除了这个地方,各个模块之间几乎没有数据交互的机会 ### 运行流程 以全局定位为例 1. 应用程序初始化(APP_Config) 1. 应用程序类成员函数绑定 1. 应用程序类初始化(Method_Init) 1. 所属模块属性配置(/* Property */) 3. 模块初始化(Module_Config) 1. 模块类成员函数绑定 1. 模块类初始化(Method_Init) 2. 应用程序运行(Method_Run) 1. 创建线程,创建信号量 3. 各个线程运行 # 下一步工作 ## 硬件 ### 修改现有问题 - RS232 5V供电接口失败 ![image.png](https://cdn.nlark.com/yuque/0/2021/png/427268/1609749172999-4d5ccd78-b03b-47a9-a6f4-a85811b3486d.png#align=left&display=inline&height=261&margin=%5Bobject%20Object%5D&name=image.png&originHeight=507&originWidth=795&size=248695&status=done&style=none&width=410) - 全局定位在车上的供电方式为由主控板的232串口通信线供电 - 现从这里供电无反应,可排查是否是线路问题 - 全局定位上的悬臂上的磁编码器PCB需要重新做 ![image.png](https://cdn.nlark.com/yuque/0/2021/png/427268/1609749942099-f8a013f9-1f24-47cf-886d-2e603f70e2b1.png#align=left&display=inline&height=220&margin=%5Bobject%20Object%5D&name=image.png&originHeight=499&originWidth=574&size=343395&status=done&style=none&width=253) - 原来的AS5048a芯片数据不好(可能是远距离运输导致) - 更换了模块,用双面胶贴上去的,距离磁钢较远,而且紧固螺丝不能完全旋入,会受影响 ![image.png](https://cdn.nlark.com/yuque/0/2021/png/427268/1609749493978-ec51a141-8173-4c63-b7ad-7390fcca35ab.png#align=left&display=inline&height=68&margin=%5Bobject%20Object%5D&name=image.png&originHeight=68&originWidth=74&size=13854&status=done&style=none&width=74) - 重新做一块装上去,注意PCB的尺寸(为特殊形状) ### 升级需要 - 陀螺仪是否需要更换,可测试后讨论 - 现在是MPU9250 - 如果没有把握,建议单独留出一个接口,支持外接陀螺仪对比效果 - 程序上很好处理,更换数据来源即可 ## 软件 ### 测试数据准确性 - 将全局定位反馈数据(上位机或者RTT Finsh打印)与装置的实际位置对比 - 有三种方法实现 - 1. 手动读数,标记刻度,角度,对比数据 - 2. 通过控制装置的电机,换算齿比后,得到装置位置,对比数据 - 3. 装置上再加装原全局定位,读取返回数据,对比 ### 完善外设接口 - RS232与主控的通信还没有写 - 建议遵从原全局定位的通信协议,实现适配,无需修改底盘主控的程序 - 需要设置一个开关调整串口在不同场合下的输出,提升运算性能 - 调试中,仅开启上位机串口和Finsh串口发送数据 - 运行中,仅开启RS232串口发送数据 ### 加装外部校准手段 - 磁力计 - 如果可用磁力计校准,可以使用现已有的Flash文件系统,存取滤波参数 - 考虑磁力计在现实情况下的表现 - 激光校准 - 设置开关,在某个情景下读取激光参数,作为陀螺仪数据,并旋转坐标矩阵 # 写在最后 - 从程序结构那个地方,可能看的比较费劲,因为每个人的实践经历不同,对同一件事的看法也不同 - 这种写法也是我第一次,公开的,完整的描述 - 之前从各个模块的编写中或多或少流露出这种组织思路 - 但真正能形成一个整体,还是从组织成为一个应用程序开始 - 最开始所有的想法都来源于一篇这样的文章 [【编程之美】常用于单片机的接口适配器模式C语言实现.pdf ](https://www.yuque.com/office/yuque/0/2020/pdf/427268/1591106122978-fe901cce-9f53-407e-a9e1-9fb9b7dd6b0d.pdf?from=https%3A%2F%2Fwww.yuque.com%2Fwangxi_chn%2Fqaxke0%2Foz4zyh) - 读过之后就发现,这样可以解决我一直比较困惑的问题 - 如何提升代码的复用性 - 这种复用性不是说 复制然后粘贴 就好用的那种 - 而是实实在在,可以成为积木,变成某个应用程序的一部分 - 并且支持“热修复” - 这里的热修复指的是,三个应用程序使用了同一个模块,当我想要添加这个模块的功能,直接修改这个模块的文件即可,不必修改三个应用程序的三个地方 - 而且模块接口尽可能简洁明了 - 最重要的是,支持面向对象思想 - C语言不像C++和C#那样,提供语法来支持类、对象、属性、方法这些概念 - 但我们可以通过结构体等数据结构来模拟,来封装,极大程度上减少工作量 - 最主要的是,我的单片机程序(RTT支持)有了一个固定的编程模板,通过这个模板可以清楚的理解每个应用在干什么 - 不论是全局定位、测试装置还是分布小模块,他们的框架都是一样的 - 不同之处仅在于,调用了不同的模块,进行了不同的计算 - 这也是为什么我可以同时写三个工程而不觉得混乱,算上上位机,四台设备的通信调试也可以清楚分析 - 这个框架是看了那篇文章后写了一个LED的模块,逐渐衍生出来的 - 不是突然一下确定下来的,包括里面变量的命名、概念的区分,都是随着实践逐渐完善 - 可以看到部分模块的命名并不是遵从上述规律,是因为时间久远 - 最新的模块一定是符合上述标准的 - 通过这种方式也形成了自己的代码风格,基本上能实现一看就认出是我写的 - 后来因为毕业设计要学习Linux的内核源码 - 发现其实Linux的设备驱动框架和这个思路差不多,那篇文章应该也是借鉴了Linux的写法 - 所以一个大的工程、标准的工程,不只是功能的实现,它的结构,它的组织形式一定是考究的 - 我们的机器人控制,当然控制算法很重要,但是有没有一种途径让控制算法保留成模块 - 我们也写了很多驱动,但是有多少驱动是反复的写,反复的调试 - 有了RTT,我们可以掩藏一些硬件细节,同样也对我们把各个功能集成为模块提供了契机 - 所以,你可能不喜欢这种应用程序的写法,也可能不喜欢这种模块的组织形式 - 但一定要有这种想法和趋势,来提升自己的效率,让曾经的工作积累为自己铺路