0%

CAN通信稳定要求

目前已知机器人会在以下几种情况中由于CAN异常出现控制电机不稳定的现象

  • 电机已经离线,PID进程仍然在继续,当电机恢复在线时出现失控
  • CAN完全离线,但是仍然在中断里执行有阻塞的发送进程,导致中断系统卡死
  • CAN总线上没有一个可应答的电机时,持续向CAN总线上发送数据会使STM32中的CAN模块由于发送应答错误过多而进入离线(Bus-off),目前已知该状态即使在自动离线管理模式(ABOM)打开的情况下也不能可靠地自动恢复
  • CAN总线控制数据发送ID过大,在仲裁优先级里面排最低,在没有自动重传的情况下会出现连续掉帧的情况
  • STM32 一个CAN缓冲区只有三个位置,当高速接收时可能会出现Overrun

针对上述问题,在STM32通过CAN控制电机过程中应该:

  • 在CAN的配置中打开自动重传(NART清0,注意NART意思是禁用自动重传,清0代表关闭禁用自动重传,在CubeMX图形界面配置时应该要区分开)以及自动离线管理(ABOM)
  • 在分配CAN的ID时尽量不使用GM6020中0x2FF的反馈ID
  • 在没有收到电机反馈数据不要往CAN总线上发任何数据,即使是没有用PID控制的电机
  • 确保使用的HAL库的CAN发送函数不带阻塞(已知目前最新版HAL不存在这个问题)
  • 在main函数的主循环内用HAL_CAN_GetState不断检测CAN是否处于离线模式(BOF),如果是,则重新启动CAN
  • 打开CAN的过滤器,将不同ID的包分开到两个缓冲区
阅读全文 »

现象

在用遥控器控制或静止一段长时间后,哨兵6020电机云台自发地抖动。用手拧云台发现控制过程中的力矩时常不均匀,甚至出现一定角度范围内云台电机无力的情况。这种情况同时出现在Pitch和Yaw轴电机上。

原因分析

由于CAN总线上可能出现多个设备同时发送数据的情况,而同一时刻CAN总线只能被一个设备占用,因此会出现总线争夺的问题。CAN为了解决这一问题,在总线上有一套规则决定哪一方拥有占用总线的权利,我们称它为总线仲裁。控制电机发送频率越高,CAN总线越容易出现冲突。当CAN的带宽不够用的时候仲裁失败必定有一方出现仲裁失败(CAN一帧为111bit,在1Mbps总线上的带宽约为9帧/毫秒,经过计算目前暂时没有发现有机器人出现这个问题,但是对于较多电机的机器人来说把控制频率改高可能会超过总线带宽,使某些帧总是丢失)。仲裁的结果并不是随机的,CAN发送用的ID越小其优先级越高。当小ID与大ID发送冲突时,仲裁的结果是小ID发送成功,大ID发送失败。而在STM32里面仲裁失败不算发送错误,当出现仲裁失败时ALST位将置1(图1),因此在STM32的错误寄存器里看不到有问题。测试时也去确实发现ALST0出现为1的情况
pic

pic
在哨兵电机ID配置中,云台电机被配置为5和6,发送包的ID为0x2FF刚好比所有电机反馈数据的ID都大(见C620GM6020文档,电机反馈的ID在0x201~0x20B之间),因此出现仲裁情况时第一个失败的是这一帧。偶尔出现丢失不会引起致命的问题,但是控制方4ms发送一帧,当两者发送频率出现同步时,由于4ms电机反馈周期1ms的整数倍,两者冲突的时间会极长,而每次都是STM32发送方仲裁失败,一段时间之后两者发送时刻错开,STM32又能够在总线空闲时发送,因此能明显发现电机出现无力,但又能恢复的情况。而其他机器人如步兵没有使用以0x2FF为ID的帧做控制,其他的ID如0x1FF和0x200都比电机ID小,因此发送方不可能出现仲裁失败。

阅读全文 »

STM32中的SVC_Handler

SVC:Supervisor Call,指令用于产生一个SVC异常。它是用户模式代码中的主进程,用于创造对特权操作系统代码的调用。SVC是用于呼叫操作系统所提供API的正道。用户程序只需知道传递给操作系统的参数,而不必知道各API函数的地址。
用途:

  1. 用于在Unprivileged模式下执行Privileged的代码
  2. 中断优先级较高,用于执行一些不能被中断打断的代码(也可以用CPSIE,CPSID指令完成)

参考:https://blog.stratifylabs.co/device/2013-10-12-Effective-Use-of-ARM-Cortex-M3-SVCall/
实例:http://www.keil.com/download/files/stm32_svc.zip

STM32中的PendSV

阅读全文 »

在C语言里一种有效组织数据的方式就是结构体,除了储存多种类型的数据外,结构体也是使用一个协议的数据格式最好的办法。这样做还有一个好处就是上位机采用同样的结构体即可通信,在代码层面的移植相当容易。例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct{
int a,b;
char c;
float f;
}data_t;

data_t bundle;
//给各个数值输入值
bundle.a = 1;
bundle.b = 2;
bundle.c = 'a';
bundle.f = 0.0f;
//此处采用转换指针类型的形式调用如串口等发送数据
uart_send((unsigned char*)&bundle);

然而有个问题,结构体内部的变量并不是紧密的排在一起,他们中间默认是有空隙的。在上面的代码中sizeof(data_t)的值并不是4+4+1+4,而是在char c后面增加了三个字节的空位。这就是内存对齐的问题,本质的原因是硬件上为了减少访问次数,通常会将变量按照4字节对齐。

通俗一点的解释办法,这个原则就是要求单个变量不能跨4字节(当然双精度对于这个原则不适用,但对于四字节及四字节以下的变量,这个原则却是正确的)。那么有没有变量连续储存的结构体呢?由于内存对齐实际上是编译器的优化,而不是硬件的强制要求,所以我们可以配置编译器让它在以牺牲访问次数的代价来换取这种对齐模式。参照编译器手册,我们可以利用#pragma pack(1)来完成或者在struct前加__PACKED

参考:https://zhuanlan.zhihu.com/p/30007037

阅读全文 »

串口接收

串口是一种直接接收串行数据的方法,而通常固定数据帧的结构为多个字节,在处理的时候实际上是针对整帧。因此需要通过某种方法将串行数据转换为相当于同一时刻连续出现的数据处理。评定这种转换方法的优劣有以下方面

  • 正确性
    能正确识别数据帧的帧头和帧尾,总是可以将接收到的各种数据正确转换,不论发送的间隔,发送的时机如何,甚至也可以处理不定长的数据
  • 可靠性
    当发送数据出现不符合协议规定的数据时能够判断出数据错误,并返回错误信息,同时处理该错误时不会影响到接下来的接收过程
  • 完整性
    在保证以上两点的情况下尽可能减少数据丢失

实际上,在STM32的串口中,我们接收数据通常会采用以下两种方法

  • 串口中断接收
    当每接收到一个字节时发生一次中断,处理一个字节,通常会编写一个有限状态机(FSM)去识别和处理一帧数据。这种方法是最原始的,最通用的接收方法,但是每接收一个字节都需要占用内核时间,同时不容易编写发现和处理错误的代码段,因此目前没有被我们使用
  • 串口DMA接收
    DMA是直接内存访问(Direct Memory Access)的缩写。顾名思义,它是一个直接访问处理内存数据的外设,通过预先配置可以让该外设自动传输内存中的数据而无需内核干预。在串口的应用中,在接收到一个字节后DMA将串口数据寄存器中的数据直接复制到指定内存地址中,这个内存地址可以预先设定,并且可以配置DMA在每次完成传输后地址自增1,直到达到预设的数据长度为止。于是我们可以设定一块缓冲区,在正确配置DMA的情况下即可实现将接收到的数据连续填入这个缓冲区内,整个过程由DMA控制器完成。DMA本身可以配置在传输数据过半(完成预先设定的数据长度的一半)或者完成所有传输后发出中断,以便内核及时处理缓冲区内的数据。
阅读全文 »

工具集

​ IDA, gdb & pwndbg, checksec, pwntools, LibcSearcher

漏洞点

拿到程序后用checksec看,64位ELF,发现没有主程序canary,NX以及ALSR

1
2
3
4
5
6
7
8
9
10
11
12
[*] '/home/ctf/pwn/pwn1'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] '/home/ctf/pwn/libc-2.23.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
阅读全文 »

simple

x64的ELF,主函数反编译

xor

xor函数是将两个数组异或后放回第二个数组arr内,同时判断flag是否正确有两个条件,一个是长度为26字节,一个是异或后数组与src相等。因此将src和arr异或即可解出原来的字符串。

arr

阅读全文 »

看门狗使用

独立看门狗(IWDG):通常检测硬件导致的运行出错,独立时钟运行
窗口看门狗(WWDG):通常检测软件漏洞导致的死机死循环,早喂狗晚喂狗都会复位,复位前可产生中断以便保存数据

如何喂狗

​ 正确使用看门狗定时器,但是它并不像重新载入计数器那样简单(通常被称为喂狗或者踢狗)。在其系统中运行看门狗定时器时,开发人员必须仔细选择看门狗的超时时间,以便看门狗在发生故障的系统可以执行任何不可逆转的恶意动作之前进行干预。

在简单的应用中,特别是没有使用RTOS,开发人员通常会从主循环(main loop)中提供看门狗。该方法仅需要配置适当的初始计数器值,它可以简单地选择任何超过整个主循环最坏的执行时间的值,至少有一个计时器周期。这通常是一个非常有效的方法,虽然有一些系统需要立即恢复,但更多系统只需要确保它们不会被无限期地挂起,这一方法能很好的实现之一目的。

调试技巧

  1. 上电后(或初始化完成后)点亮一枚LED可判断上述过程是否有误(复位或初始化代码问题排查),运行main函数的死循环时让LED闪烁可以判断系统此时运行状况(有效判断操作执行是否异常)
  2. 完成前期模块测试后先使用模拟器检查软件运行是否正确,后再烧写上机调试
  3. 通过对某个GPIO引脚产生逻辑电平作为调试信号,辅之以示波器可以调试某些对时序性较高的过程代码
  4. Keil的调试技巧(内存读写断点,条件/次数断点,运行时在断点用Command输出字符)

优化提示

  1. 在本机仿真或用某些调试器时可能有Profiler工具查看执行时间与调用次数,方便确定优化目标
  2. 在开发阶段务必将编译器优化关掉,因为某些编译器优化会使代码执行结果异常(必要时使用volatile变量)
  3. 使用查询表(函数指针的数列)代替switch,化判断为寻址
  4. 使用全局变量来传入参数(但程序模块化下降),特别是浮点数等较大的数据类型;使用register修饰变量可针对频繁读取的变量优化
  5. 使用内联函数优化某些微小的函数,降低堆栈传参带来的额外开销
  6. 适配堆栈大小,使程序在正常运行的情况下不浪费内存(但注意堆栈溢出会带来致命的后果)
    做法:在函数内定义变量如int a[n];并赋一个特殊值,调整n的大小直到程序完整运行的时候崩溃(崩溃后应该进行跟踪),以此估算所需堆栈大小
    减小堆栈大小:避免递归算法,减小局部变量的大小,内存对齐问题
  7. 避免浮点运算或用硬件指令代替;手动写汇编

可靠性改进

阅读全文 »