程序迭代记录
Gitee
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类似
- 可详细参考代码仓库中源文件
补充说明
- 项目基于硬件版本2.0
- 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端上位机通信
补充说明
- 项目移交
- 现总结项目结构和程序说明
应用程序结构
类
模块类
- 所有的模块器件都被抽象成为类,使用C语言的结构体实现
- 结构体中有成员变量,有成员函数
- 与应用程序处于不同的代码仓库下,支持单独更新升级
以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中进行,成为该应用程序几乎唯一的全局变量
- 在Debug中,将此变量添加在监视窗口,可观察所有模块的运行状态
- 应用类的全局方法 APP_XXX_Config
- 应用类成员函数的绑定
应用类所有模块的Config
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、串口屏等,发送显示信息
- XXXLed_thread
- 各个模块相互作用的机会仅存在于应用线程这一层,除了这个地方,各个模块之间几乎没有数据交互的机会
运行流程
以全局定位为例
- 应用程序初始化(APP_Config)
- 应用程序类成员函数绑定
- 应用程序类初始化(Method_Init)
- 所属模块属性配置(/ Property /)
- 模块初始化(Module_Config)
- 模块类成员函数绑定
- 模块类初始化(Method_Init)
- 应用程序运行(Method_Run)
- 创建线程,创建信号量
- 各个线程运行
下一步工作
硬件
修改现有问题
- RS232 5V供电接口失败
- 全局定位在车上的供电方式为由主控板的232串口通信线供电
- 现从这里供电无反应,可排查是否是线路问题
- 全局定位上的悬臂上的磁编码器PCB需要重新做
- 原来的AS5048a芯片数据不好(可能是远距离运输导致)
- 更换了模块,用双面胶贴上去的,距离磁钢较远,而且紧固螺丝不能完全旋入,会受影响
- 陀螺仪是否需要更换,可测试后讨论
- 现在是MPU9250
如果没有把握,建议单独留出一个接口,支持外接陀螺仪对比效果
将全局定位反馈数据(上位机或者RTT Finsh打印)与装置的实际位置对比
有三种方法实现
RS232与主控的通信还没有写
- 建议遵从东大全局定位的通信协议,实现适配,无需修改底盘主控的程序
需要设置一个开关调整串口在不同场合下的输出,提升运算性能
磁力计
- 如果可用磁力计校准,可以使用现已有的Flash文件系统,存取滤波参数
- 考虑磁力计在现实情况下的表现
激光校准
从程序结构那个地方,可能看的比较费劲,因为每个人的实践经历不同,对同一件事的看法也不同
- 这种写法也是我第一次,公开的,完整的描述
- 之前从各个模块的编写中或多或少流露出这种组织思路
- 但真正能形成一个整体,还是从组织成为一个应用程序开始
- 最开始所有的想法都来源于一篇这样的文章
- 读过之后就发现,这样可以解决我一直比较困惑的问题
- 如何提升代码的复用性
- 这种复用性不是说 复制然后粘贴 就好用的那种
- 而是实实在在,可以成为积木,变成某个应用程序的一部分
- 并且支持“热修复”
- 这里的热修复指的是,三个应用程序使用了同一个模块,当我想要添加这个模块的功能,直接修改这个模块的文件即可,不必修改三个应用程序的三个地方
- 而且模块接口尽可能简洁明了
- 最重要的是,支持面向对象思想
- C语言不像C++和C#那样,提供语法来支持类、对象、属性、方法这些概念
- 但我们可以通过结构体等数据结构来模拟,来封装,极大程度上减少工作量
- 最主要的是,我的单片机程序(RTT支持)有了一个固定的编程模板,通过这个模板可以清楚的理解每个应用在干什么
- 不论是全局定位、测试装置还是分布小模块,他们的框架都是一样的
- 不同之处仅在于,调用了不同的模块,进行了不同的计算
- 这也是为什么我可以同时写三个工程而不觉得混乱,算上上位机,四台设备的通信调试也可以清楚分析
- 这个框架是看了那篇文章后写了一个LED的模块,逐渐衍生出来的
- 不是突然一下确定下来的,包括里面变量的命名、概念的区分,都是随着实践逐渐完善
- 可以看到部分模块的命名并不是遵从上述规律,是因为时间久远
- 最新的模块一定是符合上述标准的
- 通过这种方式也形成了自己的代码风格,基本上能实现一看就认出是我写的
- 后来因为毕业设计要学习Linux的内核源码
- 发现其实Linux的设备驱动框架和这个思路差不多,那篇文章应该也是借鉴了Linux的写法
- 所以一个大的工程、标准的工程,不只是功能的实现,它的结构,它的组织形式一定是考究的
- 我们的机器人控制,当然控制算法很重要,但是有没有一种途径让控制算法保留成模块
- 我们也写了很多驱动,但是有多少驱动是反复的写,反复的调试
- 有了RTT,我们可以掩藏一些硬件细节,同样也对我们把各个功能集成为模块提供了契机
- 所以,你可能不喜欢这种应用程序的写法,也可能不喜欢这种模块的组织形式
- 但一定要有这种想法和趋势,来提升自己的效率,让曾经的工作积累为自己铺路