程序迭代记录

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说明

补充说明

  • 项目基于硬件版本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端上位机通信

补充说明

  • 项目移交
  • 现总结项目结构和程序说明

应用程序结构

【RT-Thread】平面全方位定位系统应用程序 - 图1

模块类

  • 所有的模块器件都被抽象成为类,使用C语言的结构体实现
  • 结构体中有成员变量,有成员函数
  • 与应用程序处于不同的代码仓库下,支持单独更新升级
  • 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.

    1. 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;

  1. - 注释/* Property */下的,为使用该结构体声明一个变量时(即类的例化),必须给出的初始值(即类的属性)
  2. - 一般为与硬件或RTT的对应关系
  3. - 注释/* Value */下的,为一些变量,不必赋予初值,会在程序运行中发生变化
  4. - 一般是一些中间计算结果,或者是输出结果
  5. - 可用来观察,调用,调试
  6. - 注释/* Method */下的,为成员函数(即类的方法),该模块提供的一些程序段
  7. - 使用 . 成员函数 即可实现对其的调用
  8. - 所用的模块都对成员函数有固定含义的命名
  9. - Method_Init 一般为对该模块的初始化,用户一般不直接使用该方法
  10. - (在该模块的全局函数Config中自行运行)
  11. - Method_Handle 一般为该模块的中断服务函数,用户一般需要将其放到一个线程中,会自行阻塞运行
  12. - Method_Set 一般为该模块的参数设置,可根据模块功能和参数组成决定
  13. - Method_Get 一般为获取该模块的某个数值
  14. **注意:由于这里是使用了函数指针作为结构体成员,代替实现了类似于方法的功能,但是还需要将实际的函数与函数指针完成映射,映射过程是在每个模块的全局函数 ****Module_XXX_Config 中实现的,同时模块的 .Init 方法也在这里被调用**
  15. ```c
  16. /* Global Method */
  17. rt_err_t Module_Led_Config(MODULE_LED *Dev_LED){
  18. if(Dev_LED->Method_Init==NULL &&
  19. Dev_LED->Method_Set==NULL &&
  20. Dev_LED->Method_Handle==NULL
  21. ){
  22. /* Link the Method */
  23. Dev_LED->Method_Init = Module_LedInit;
  24. Dev_LED->Method_Set = Module_LedSet;
  25. Dev_LED->Method_Handle = Module_LedHandle;
  26. }
  27. else{
  28. rt_kprintf("Warning: Module Led is Configed twice\n");
  29. return RT_ERROR;
  30. }
  31. /* Device Init */
  32. Dev_LED->Method_Init(Dev_LED);
  33. return RT_EOK;
  34. }

因此在使用各个模块前必须调用该函数,实现对模块的初始化,否则会在RTT中报堆栈溢出的错误

应用类

  • 将整个应用程序也抽象成为了一个类 _APP_GLOBALPOS
  • 类的成员即为各个模块,实现类的嵌套(结构体嵌套)
  • 类的方法只有两个
    • Method_Init 应用程序初始化(不由用户调用,Config自行完成)
    • Method_Run 应用程序运行
  • 这两个方法仅在main中被调用,作为应用程序的入口
  • 对该应用类的例化(结构体变量声明)也在main中进行,成为该应用程序几乎唯一的全局变量

image.png

  • 在Debug中,将此变量添加在监视窗口,可观察所有模块的运行状态
    • 应用类的全局方法 APP_XXX_Config
  • 应用类成员函数的绑定
  • 应用类所有模块的Config

    1. void APP_GlobalPos_Config(APP_GLOBALPOS *Application)
    2. {
    3. if( Application->Method_Init == NULL &&
    4. Application->Method_Run == NULL
    5. ){
    6. /* Link the Method */
    7. Application->Method_Init = APP_GlobalPosInit;
    8. Application->Method_Run = APP_GlobalPosRun;
    9. }
    10. else{
    11. rt_kprintf("Warning: Module Led is Configed twice\n");
    12. return;
    13. }
    14. /* Device Init */
    15. Application->Method_Init(Application);
    16. /* Module Config */
    17. Module_Led_Config(&(Application->dev_Led));
    18. Module_File_Config(&(Application->dev_SpiFile));
    19. Module_UartCom_Config(&(Application->dev_UartBsp));
    20. Module_MPU9250_Config(&(Application->dev_Mpu9250));
    21. Module_AS5048_Config(&(Application->dev_As5048_left));
    22. Module_AS5048_Config(&(Application->dev_As5048_right));
    23. return;
    24. }
  • 在应用类的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. 应用程序类成员函数绑定
    2. 应用程序类初始化(Method_Init)
      1. 所属模块属性配置(/ Property /)
    3. 模块初始化(Module_Config)
      1. 模块类成员函数绑定
      2. 模块类初始化(Method_Init)
  2. 应用程序运行(Method_Run)
    1. 创建线程,创建信号量
  3. 各个线程运行

    下一步工作

    硬件

    修改现有问题

  • RS232 5V供电接口失败

image.png

  • 全局定位在车上的供电方式为由主控板的232串口通信线供电
  • 现从这里供电无反应,可排查是否是线路问题
    • 全局定位上的悬臂上的磁编码器PCB需要重新做

image.png

  • 原来的AS5048a芯片数据不好(可能是远距离运输导致)
  • 更换了模块,用双面胶贴上去的,距离磁钢较远,而且紧固螺丝不能完全旋入,会受影响

image.png

  • 重新做一块装上去,注意PCB的尺寸(为特殊形状)

    升级需要

  • 陀螺仪是否需要更换,可测试后讨论
    • 现在是MPU9250
  • 如果没有把握,建议单独留出一个接口,支持外接陀螺仪对比效果

    • 程序上很好处理,更换数据来源即可

      软件

      测试数据准确性

  • 将全局定位反馈数据(上位机或者RTT Finsh打印)与装置的实际位置对比

  • 有三种方法实现

      1. 手动读数,标记刻度,角度,对比数据
      1. 通过控制装置的电机,换算齿比后,得到装置位置,对比数据
      1. 装置上再加装东大的全局定位,读取返回数据,对比

        完善外设接口

  • RS232与主控的通信还没有写

  • 建议遵从东大全局定位的通信协议,实现适配,无需修改底盘主控的程序
  • 需要设置一个开关调整串口在不同场合下的输出,提升运算性能

    • 调试中,仅开启上位机串口和Finsh串口发送数据
    • 运行中,仅开启RS232串口发送数据

      加装外部校准手段

  • 磁力计

    • 如果可用磁力计校准,可以使用现已有的Flash文件系统,存取滤波参数
    • 考虑磁力计在现实情况下的表现
  • 激光校准

    • 设置开关,在某个情景下读取激光参数,作为陀螺仪数据,并旋转坐标矩阵

      写在最后

  • 从程序结构那个地方,可能看的比较费劲,因为每个人的实践经历不同,对同一件事的看法也不同

  • 这种写法也是我第一次,公开的,完整的描述
  • 之前从各个模块的编写中或多或少流露出这种组织思路
  • 但真正能形成一个整体,还是从组织成为一个应用程序开始
  • 最开始所有的想法都来源于一篇这样的文章

【编程之美】常用于单片机的接口适配器模式C语言实现.pdf

  • 读过之后就发现,这样可以解决我一直比较困惑的问题
    • 如何提升代码的复用性
  • 这种复用性不是说 复制然后粘贴 就好用的那种
  • 而是实实在在,可以成为积木,变成某个应用程序的一部分
  • 并且支持“热修复”
  • 这里的热修复指的是,三个应用程序使用了同一个模块,当我想要添加这个模块的功能,直接修改这个模块的文件即可,不必修改三个应用程序的三个地方
  • 而且模块接口尽可能简洁明了
  • 最重要的是,支持面向对象思想
  • C语言不像C++和C#那样,提供语法来支持类、对象、属性、方法这些概念
  • 但我们可以通过结构体等数据结构来模拟,来封装,极大程度上减少工作量
  • 最主要的是,我的单片机程序(RTT支持)有了一个固定的编程模板,通过这个模板可以清楚的理解每个应用在干什么
  • 不论是全局定位、测试装置还是分布小模块,他们的框架都是一样的
  • 不同之处仅在于,调用了不同的模块,进行了不同的计算
  • 这也是为什么我可以同时写三个工程而不觉得混乱,算上上位机,四台设备的通信调试也可以清楚分析
  • 这个框架是看了那篇文章后写了一个LED的模块,逐渐衍生出来的
  • 不是突然一下确定下来的,包括里面变量的命名、概念的区分,都是随着实践逐渐完善
  • 可以看到部分模块的命名并不是遵从上述规律,是因为时间久远
  • 最新的模块一定是符合上述标准的
  • 通过这种方式也形成了自己的代码风格,基本上能实现一看就认出是我写的
  • 后来因为毕业设计要学习Linux的内核源码
  • 发现其实Linux的设备驱动框架和这个思路差不多,那篇文章应该也是借鉴了Linux的写法
  • 所以一个大的工程、标准的工程,不只是功能的实现,它的结构,它的组织形式一定是考究的
  • 我们的机器人控制,当然控制算法很重要,但是有没有一种途径让控制算法保留成模块
  • 我们也写了很多驱动,但是有多少驱动是反复的写,反复的调试
  • 有了RTT,我们可以掩藏一些硬件细节,同样也对我们把各个功能集成为模块提供了契机
  • 所以,你可能不喜欢这种应用程序的写法,也可能不喜欢这种模块的组织形式
  • 但一定要有这种想法和趋势,来提升自己的效率,让曾经的工作积累为自己铺路