华北五省管道检测比赛分享-K210与stm32通信(比赛分享二)
前言
这篇文章是为了记录我独自一人参加这个比赛的经历,其中涉及到的可能对大家有帮助的源码以及知识我将会进行开源与分享(源码放在了文章结尾处)。
一、最后的成品大致展示
成品大致就是这样啦,由于是我一个人参与的制作,再加上只有大概两个周的时间(其中每天还要上课),可能会有很多不足之处,大家可以大胆进行指出哦。
二、作品的制作过程
这一部分,我会结合我的思路进行分模块的分享。
1、整体的制作思路
(1)、设备的选型:
由于没有钱(哈哈),我就在实验室里捡垃圾凑了一套。机身选择的是瓶口较宽的枸杞瓶,机臂使用的是3D打印,视觉部分我使用的是K210,控制部分我采用朴实无华的c8t6进行控制,为了解决防水的问题,加上没有报销我就直接捡了两个很便宜的穿越机电机以及两个小电调进行差速控制。
(2)、代码部分思路:
a、视觉方面:
视觉我选择采用k210完成白色管道的循迹以及漏油点的识别(完整代码我放在最后)。
# 定义颜色阈值
WHITE_THRESHOLD_LAB = (40, 73, -55, 7, -2, 50) # 白色检测阈值
BLACK_THRESHOLD_LAB = (21, 39, -33, 17, 3, 24) # LAB阈值,确保包含黑色
# 采用库函数进行相应颜色的筛选
blobs = img.find_blobs([WHITE_THRESHOLD_LAB], pixels_threshold=500, area_threshold=500, merge=True)
经过我的验证,在水下环境里采用LAB调阈值的方法相较于灰度图比较鲁棒(在关自动曝光等一些列自适应后)。巡线的思路很简单,因为我采用的是差速控制,我将识别到的最大白色区域的中心x轴坐标减去图像的中心的x轴坐标,得到一个有符号的浮点数误差值,使用数列对近几次的数据进行均值滤波得到较为准确的误差(这里大家可以使用别的更为有效的滤波算法,比如说卷积滤波),最后将误差通过uart传给stm32。识别漏油点也是类似道理,由于漏油点是存在于白色管道上方,我将漏油点的感兴趣区域设置为白色管道的识别区域。
b、电控方面:
电控通过串口接收到有符号的浮点数误差,将误差作用于转向环PID实现对电机的差速控制。
c、机械方面:
机械方面由于设备比较差,所以我尽可能的对它进行了重心配重处理以及设计了尾舵(虽然没来得及打印哈哈哈)。
2、具体的代码实现与讲解
(1)、首先是K210与STM32的通信:
a、K210端:
!!!注意下述的波特率要一直且一定不能太高(如115200),会导致数据传输有问题,实测9600到14400比较稳定,RX与TX交叉交叉连接。
# 导包
from fpioa_manager import fm
from machine import UART
# 初始化串口
fm.register(11, fm.fpioa.UART1_RX, force=True) # 设置引脚映射,11为RX引脚
fm.register(10, fm.fpioa.UART1_TX, force=True) # 设置引脚映射,10为TX引脚
uart = UART(UART.UART1, 14400, 8, None, 1, timeout=1000, read_buf_len=4096) # 初始化UART1
# 定义帧头和帧尾
frame_header = b'\x48' # 帧头 'H'
frame_tail = b'\x54' # 帧尾 'T'
# 构建并发送数据帧
payload = "{:.2f}".format(average_deviation) #average_deviation 是指计算出的误差
frame = frame_header + payload.encode() + frame_tail
uart.write(frame) # 通过 UART 发送帧
这里整体的思路是将得到的误差(通过format)格式化为两位小数的字符串,再通过(.encode)将字符串转化为字节,最后加入帧头和帧尾拼接成完整的字节帧,通过串口进行发送(发送的帧头和帧尾实则上是对应的ASCLL值 );
b、STM32端:
由于发送过来的数据是有符号的,那么负数就会多出一位(即负号)。这边我采用的是利用DMA接收不定长数据,再通过判断帧头帧尾是否接收到了完整数据,具体实现步骤如下:
#define FRAME_HEADER 'H' // 帧头
#define FRAME_TAIL 'T' // 帧尾
#define RX_BUFFER_SIZE 50
double g_final_error = 0.0;
uint8_t uart_rx_buffer[RX_BUFFER_SIZE] = {0}; // 定义接收缓冲区
uint8_t frame_buffer[RX_BUFFER_SIZE]; // 帧数据缓冲区
volatile uint8_t frame_ready = 0; // 标志位,表示是否接收到完整帧
uint16_t frame_length = 0; // 实际接收到的有效数据长度
int i = 0;
/* 书写中断回调函数 */
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart == &huart2)
{
int frame_found = 0;
// 遍历接收到的数据,查找帧头和帧尾
for (i = 0; i < Size; i++)
{
// 查找帧头
if (uart_rx_buffer[i] == FRAME_HEADER)
{
// 查找帧尾
for (int j = i + 1; j < Size; j++)
{
if (uart_rx_buffer[j] == 0x0D || uart_rx_buffer[j] == 0x0A)
{
continue; // 忽略回车换行符
}
if (uart_rx_buffer[j] == FRAME_TAIL) // 找到帧尾
{
// 计算有效数据长度
frame_length = j - i - 1;
// 如果帧长度合理,将数据接收标志位 frame_ready 置1
if (frame_length > 0 && frame_length < RX_BUFFER_SIZE)
{
frame_ready = 1;
}
frame_found = 1;
break;
}
}
}
// 如果找到完整帧,跳出循环
if (frame_found)
{
break;
}
}
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, uart_rx_buffer, RX_BUFFER_SIZE);
__HAL_DMA_DISABLE_IT(&hdma_usart2_rx, DMA_IT_HT);
}
}
/* 下面提供主函数中相关部分代码,初始化部分省略 */
int main(void)
{
/* 开启DMA接收中断 */
HAL_UARTEx_ReceiveToIdle_DMA(&huart2, uart_rx_buffer, sizeof(uart_rx_buffer));
__HAL_DMA_DISABLE_IT(&hdma_usart2_rx, DMA_IT_HT);
while (1)
{
// 如果接收到完整帧,处理并打印数据
if (frame_ready)
{
memcpy(frame_buffer, &uart_rx_buffer[i + 1], frame_length);
g_final_error = atof((const char *)frame_buffer);
}
}
}
上述即是代码实现部分,总结来说就是:
初始化,开启DMA接收中断,书写中断回调函数(注意这里不是普通的串口接收中断,而是官方提供的另一个用于接收不定长数据的扩展函数),调用这个函数会在接收到数据长度的一半时(例如 #define RX_BUFFER_SIZE 50 上述定义数字组长度为50,在接收到25个字节时会进入此中断回调函数。这个机制是为了在数据较为庞大时提前开始进行数据的处理,这里我们的数据只是较小的浮点数,所以调用(__HAL_DMA_DISABLE_IT(&hdma_usart2_rx, DMA_IT_HT);)将其关闭防止对数据产生影响。
又因为初始化时是使用的是DMA的nomal模式,完成一次转运后就会关闭中断,所以在中断回调函数最后记得再次调用: HAL_UARTEx_ReceiveToIdle_DMA(&huart2, uart_rx_buffer, RX_BUFFER_SIZE);
上面说到K210发送的是ASCLL码值,而这里的宏定义(#define FRAME_HEADER 'H' // 帧头;#define FRAME_TAIL 'T' // 帧尾) 实际上也是ASCLL码值 ,所以在中断回调函数中才能直接进行比较寻找帧头与帧尾。
在中断函数中,我们要尽量的避免写入比较耗时的操作,防止产生中断重叠的情况以及占用较多cpu资源,比如说这里的数据处理。这里我们使用全局标志位的方式 (frame_ready ),寻找到完整数据后将其置1,在主循环中进行数据的处理。
/* 将 uart_rx_buffer[i + 1] 中的数据复制到字符串 frame_buffer 中 */
memcpy(frame_buffer, &uart_rx_buffer[i + 1], frame_length);
/* 将字符串 frame_buffer 使用 atof 转化为有符号的浮点数类型 */
g_final_error = atof((const char *)frame_buffer);
(2)、穿越机无刷电机电调的校准程序:
由于是使用的如下图穿越机电调,为了比较准确的控制以及为了电机烧毁后更换也不影响,我写了一段关于下图电调的校准程序。大体逻辑很简单:
市面上大多这种电调使用的是50Hz频率的PWM波进行控制,控制的占空比范围大概为百分之五到百分之十,当上电时超过最低一定范围时,比如说占空比为百分之八,这时候电调就会触发校准,给油也不会转动。只能等待声音过去后,将占空比拉低到百分之五,这时电调才会完成校准,才能输出想要的占空比控制其转速大小。
实现思路:
占空比 =
50Hz,对于72MHz主频的stm32c8t6来说(预分频(PSC)为72,重装载值(ARR)为20000)即输出50Hz的PWM波形;最高占空比(百分之十) -> CCR的值为2000;最低 -> CCR的值为1000;初始化时将CCR的值设为最高2000,等待几秒待自检第一段声音完成,拉低CCR的值为1000等待自检结束,这时候就能自由的控制电机转速啦。
怎么实现等几秒的功能呢,第一印象想到的就是延时函数(Delay),经过我的使用后我发现,这样长时间(十几秒)的使用Delay函数,会导致cpu无法进行误差的收发等,不便于调试。我采用的是使用定时器更新中断来解决这个问题,如下图,这样就完美的解决了这个问题。
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim == &htim3)
{
// 这里采用的是2秒进入一次更新中断
static uint8_t overflow_count = 0;
overflow_count++;
if (overflow_count == 3) /* 进入三次,即6s */
{
// 发送低油门信号
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 1000);
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, 1000);
}
else if (overflow_count == 7)
{
HAL_TIM_Base_Stop_IT(&htim3);
overflow_count = 0; // 重置计数器
}
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13); /* LED闪烁 */
}
}
结尾
链接: https://pan.baidu.com/s/1BLzlH0Om8JbWsumtwwhl2Q 提取码: 73k1
文章的最后我将源码奉上,有问题的地方大家都可以进行指出,如果觉得内容有帮助,请点赞关注评论支持一下吧!
作者:梦..