1 Star 17 Fork 9

立创开发板/裸机版基于立创梁山派的21年电赛F题智能送药小车

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
13_送药小车发挥部分.md 24.09 KB
一键复制 编辑 原始数据 按行查看 历史
yzh 提交于 2023-08-22 14:39 . -初步软件文档

1 题目发挥部分

  1. 两个小车协同运送到同一指定的中部病房,小车 1 到达病房后等待卸载药品,小车 2 识别病房房号装载药品后启动运送,到达自选暂停点后暂停,点亮黄色指示灯,等待小车 1 卸载;小车 1 卸载药品后开始返回,同时控制小车 2 熄灭黄色指示灯,继续运送。(从小车 2 启动运送开始,到小车 1 返回药房且小车 2 到达病房的总时间越短越好,不计算小车 2 黄灯亮时的暂停时间,不超过 60s)
  2. 两个小车协同运送到不同的远端病房送、取药品,小车 1 送药,小车 2 取药。小车 1 识别病房号装载药品后开始运送,小车 2 在药房处识别病房号等待小车 1 的取药开始指令;小车 1 到达病房后卸载药品,开始返回,同时向小车 2 发送启动取药指令;小车 2 收到取药指令后开始启动,到达病房后停止,亮红色指示灯(从小车 1 返回开始,到小车 1 返回到药房且小车 2 到达取药病房的总时间越短越好,不超过 60s)
  3. 其他

2 打开工程

打开工程9_car_advance,这个工程是送药小车题目中的基本要求实现,有关控制小车运动的函数都在app_car_control.c文件中,在app_car_control_run(void)函数中,通过一个在car_config.h文件中的宏定义#define CURRENT_AS_CAR1来确定这个工程是小车1的工程还是小车2的工程。就是说这个宏定义存在那么这个工程就是小车1,这个宏定义不存在那么这个工程就是小车2。让一个工程同时支持两个固件,方便后面维护。

3 实现思路

在送药小车基础部分的实现中,我们已经能让小车在地图中顺利运动了,也可以实现基础部分的要求了。可以看到在上面的要求中,最重要的就是两个小车要可以互相通讯,小车2要知道小车1的位置才能正确选择自选暂停点。小车1要能给小车2发送取药指令。

在这里我用的是国产沁恒的CH9143蓝牙串口USB三向通讯模块。他的主要优点有两个:

  1. 配对方便,它有智能配对功能,只要把两个处于主从一体的模块在3s内同时上电就可以配对成功了。
  2. 支持USB,一个模块连到单片机TTL串口,另一个模块通过USB连到电脑,就可以远程控制了,把数据曲线直接通过串口发送过来我们就可以无线调试了,很方便。

在 **立创梁山派与K210串口通讯协议框架搭建 **中,我们已经知道了为什么要是用定义好的通讯协议,解决了如何解决串口通讯的粘包,分包,校验问题,实现我们所定义的通讯协议。在这两个小车的双向通讯的中,我们也是要将这些问题解决。

3.1 定义双车交互的通讯协议

有关的实现都在app_protocol.c文件中,小车1的发送数据就是小车2的接收数据,小车1的接收数据就是小车2的发送数据。

3.1.1 car1 to car2

发送

负载包 变量名 数据所对应的意义
payload[0] need_to_medical_ward_number 小车1要去的病房号
payload[1] medical_ward_a a病房的病房号,固定为1
payload[2] medical_ward_b b病房的病房号,固定为2
payload[3] medical_ward_c c病房的病房号,可能为3,4,5,6,7,8
payload[4] medical_ward_d d病房的病房号,可能为3,4,5,6,7,8
payload[5] medical_ward_e e病房的病房号,可能为3,4,5,6,7,8
payload[6] medical_ward_f f病房的病房号,可能为3,4,5,6,7,8
payload[7] medical_ward_g g病房的病房号,可能为3,4,5,6,7,8
payload[8] medical_ward_h h病房的病房号,可能为3,4,5,6,7,8
payload[9] car_position 小车1在地图中的关键位置,可能为1,2,3,4,5,a,b,c,d,e,f,g,h
payload[10] car1_control_car2_flag 0x00:小车1对小车2没什么想说的
0x01:小车1已经卸载掉了药品,需要小车2去取药。

接收

负载包 含义 数据所对应的意义
payload[0] car2_position 小车2在地图中的关键位置,可能为1,2,3,4,5,a,b,c,d,e,f,g,h

3.1.2 car2 to car1

发送

负载包 含义 数据所对应的意义
payload[0] car2_position 小车2在地图中的关键位置,可能为1,2,3,4,5,a,b,c,d,e,f,g,h

接收

负载包 变量名 数据所对应的意义
payload[0] need_to_medical_ward_number 小车1要去的病房号
payload[1] medical_ward_a a病房的病房号,固定为1
payload[2] medical_ward_b b病房的病房号,固定为2
payload[3] medical_ward_c c病房的病房号,可能为3,4,5,6,7,8
payload[4] medical_ward_d d病房的病房号,可能为3,4,5,6,7,8
payload[5] medical_ward_e e病房的病房号,可能为3,4,5,6,7,8
payload[6] medical_ward_f f病房的病房号,可能为3,4,5,6,7,8
payload[7] medical_ward_g g病房的病房号,可能为3,4,5,6,7,8
payload[8] medical_ward_h h病房的病房号,可能为3,4,5,6,7,8
payload[9] car_position 小车1在地图中的关键位置,可能为1,2,3,4,5,a,b,c,d,e,f,g,h
payload[10] car1_control_car2_flag 0x00:小车1对小车2没什么想说的
0x01:小车1已经卸载掉了药品,需要小车2去取药。

3.2 解决串口通讯的粘包,分包,校验问题

这个的处理和 立创梁山派与K210串口通讯协议框架搭建 中一样,使用国产RT-Thread的软件包Upacker,具体可以看那一篇文档,这里就不过多介绍upacker和ringbuffer了。

3.2.1 通讯协议在小车1和小车2中的实现

引入upacker软件包后,我们只需要对接对应串口的输入输出。

具体代码在app_protocol.c里面,在立创梁山派中无线蓝牙模块用到的串口为uart5,用到了串口的DMA接收,所以也要初始化对应的DMA。

串口5的初始化在bsp_uart.c文件中,用了多个数组来存储串口初始化相关信息,波特率改动时只需要在bsp_uart.h文件中修改对应波特率就可以了:

......
//串口5的宏定义
#define COM_UART5                         USART5
#define COM_UART5_IRQn                    USART5_IRQn
#define COM_UART5_CLK                     RCU_USART5
#define COM_UART5_TX_GPIO_CLK             RCU_GPIOG
#define COM_UART5_TX_GPIO_AF              GPIO_AF_8
#define COM_UART5_TX_PORT                 GPIOG
#define COM_UART5_TX_PIN                  GPIO_PIN_14
#define COM_UART5_RX_GPIO_CLK             RCU_GPIOG
#define COM_UART5_RX_GPIO_AF              GPIO_AF_8
#define COM_UART5_RX_PORT                 GPIOG
#define COM_UART5_RX_PIN                  GPIO_PIN_9
#define COM_UART5_BAUD                    115200U
......

3.2.2 对接串口发送接口

/**
 * @brief  ble发送接口
 * @note
 * @param  *d:
 * @param  size:
 * @retval None
 */
static void ble_protocol_send(uint8_t *buff, uint16_t len)
{
    for(uint16_t i = 0; i < len; i++)
    {
        usart_data_transmit(COM_UART5, (uint8_t)buff[i]);
        while(RESET == usart_flag_get(COM_UART5, USART_FLAG_TBE));
    }
}

上面的函数在packer初始化的时候就对接到了ble_msg_packer上面,后面调用upacker_pack函数就可以发送数据包到串口了。

// init ble packer
upacker_init(&ble_msg_packer, ble_protocol_handle_cb,
             ble_protocol_send);

实际的定时发送函数如下:

/**
 -  @brief  小车状态信息软件定时器回调函数,串口定时发送给蓝牙模块
 -  @note   None
 -  @param  None
 -  @retval None
   */
void medical_ward_info_timer_callback(void)
{
#ifdef CURRENT_AS_CAR1
    car1_medical_ward_info.need_to_medical_ward_number =
        medical_ward_info.need_to_medical_ward_number;
    car1_medical_ward_info.medical_ward_a = medical_ward_info.medical_ward_a;
    car1_medical_ward_info.medical_ward_b = medical_ward_info.medical_ward_b;
    car1_medical_ward_info.medical_ward_c = medical_ward_info.medical_ward_c;
    car1_medical_ward_info.medical_ward_d = medical_ward_info.medical_ward_d;
    car1_medical_ward_info.medical_ward_e = medical_ward_info.medical_ward_e;
    car1_medical_ward_info.medical_ward_f = medical_ward_info.medical_ward_f;
    car1_medical_ward_info.medical_ward_g = medical_ward_info.medical_ward_g;
    car1_medical_ward_info.medical_ward_h = medical_ward_info.medical_ward_h;
    car1_medical_ward_info.car1_position = car_position;
    car1_medical_ward_info.car1_control_flag = car1_control_car2_flag;

    car1_send_to_car2_info(&car1_medical_ward_info);

    get_car2_to_car1_info(&car2_medical_ward_info);

#else
    car2_medical_ward_info.car2_position = car_position;

    car2_send_to_car1_info(&car2_medical_ward_info);

    get_car1_to_car2_info(&car1_medical_ward_info);

#endif
}

上面的函数将小车1或小车2的数据刷新之后调用car1_send_to_car2_infocar2_send_to_car1_info进行实际的发送动作。

int car1_send_to_car2_info(car1_to_car2_info_t *_car1_medical_ward_info)
{
    static uint8_t temp[11];
    temp[0] = _car1_medical_ward_info->need_to_medical_ward_number;
    temp[1] = _car1_medical_ward_info->medical_ward_a;
    temp[2] = _car1_medical_ward_info->medical_ward_b;
    temp[3] = _car1_medical_ward_info->medical_ward_c;
    temp[4] = _car1_medical_ward_info->medical_ward_d;
    temp[5] = _car1_medical_ward_info->medical_ward_e;
    temp[6] = _car1_medical_ward_info->medical_ward_f;
    temp[7] = _car1_medical_ward_info->medical_ward_g;
    temp[8] = _car1_medical_ward_info->medical_ward_h;
    temp[9] = _car1_medical_ward_info->car1_position;
    temp[10] = _car1_medical_ward_info->car1_control_flag;
    upacker_pack(&ble_msg_packer, temp, 11);
    return 0;
}

int car2_send_to_car1_info(car2_to_car1_info_t *_car2_medical_ward_info)
{
    static uint8_t temp;
    temp = _car2_medical_ward_info->car2_position;
    upacker_pack(&ble_msg_packer, &temp, 1);
    return 0;
}

get_car2_to_car1_infoget_car1_to_car2_info也是将upacker解析成功后的数据定时刷新,供给小车控制使用。

3.2.3 对接串口接收接口

简单来说,送药小车接收K210传输过来的消息数据是这么流转的:串口DMA接收(如果是小车1,到达5个字节后触发DMA中断,如果是小车2,到达15个字节后触发DMA中断,和上面我们定义的通讯协议有关)->uart5的ringbuffer->系统空闲时upacker从ringbuffer中获取字符串

小车1的接收数据5个字节是1个实际串口数据+upacker使用的4个字节(包头,长度,校验)。

小车2的接收数据15个字节是11个实际串口数据+upacker使用的4个字节(包头,长度,校验)。

首先从uart5中的ringbuffer获取字符串,有数据后就调用upacker的解包函数进行解包处理

if (ring_buffer_available_read(&rb_uart5) >= 1)
{
    ring_buffer_read(&rb_uart5, &receive_char, 1);

    upacker_unpack(&ble_msg_packer, &receive_char, 1);
}

upacker成功校验之后就可以把实际的数据赋值给我们的内部变量供给其他函数使用了,下面的#ifdef CURRENT_AS_CAR1宏定义是当这个定义时,小车1接收的就是小车2的数据,没定义时就是小车2接收小车1的数据。

/**
 * @brief  ble消息解析回调
 * @note
 * @param  *buff:
 * @param  size:
 * @retval None
 */
static void ble_protocol_handle_cb(uint8_t *buff, uint16_t len)
{

    last_receive_ble_tick = get_system_tick();
#ifdef CURRENT_AS_CAR1
    // example:55 01 40 55 00
    receive_car2_medical_ward_info.car2_position = buff[0];

    // mcn_publish(MCN_HUB(car2_to_car1_info_topic),
    //             &receive_car2_medical_ward_info);
#else
    // example:55 0B C0 5D 00 01 02 00 00 00 00 00 00 00 00
    receive_car1_medical_ward_info.need_to_medical_ward_number = buff[0];
    receive_car1_medical_ward_info.medical_ward_a = buff[1];
    receive_car1_medical_ward_info.medical_ward_b = buff[2];
    receive_car1_medical_ward_info.medical_ward_c = buff[3];
    receive_car1_medical_ward_info.medical_ward_d = buff[4];
    receive_car1_medical_ward_info.medical_ward_e = buff[5];
    receive_car1_medical_ward_info.medical_ward_f = buff[6];
    receive_car1_medical_ward_info.medical_ward_g = buff[7];
    receive_car1_medical_ward_info.medical_ward_h = buff[8];
    receive_car1_medical_ward_info.car1_position = buff[9];
    receive_car1_medical_ward_info.car1_control_flag = buff[10];

    // mcn_publish(MCN_HUB(car1_to_car2_info_topic),
    //             &receive_car1_medical_ward_info);
#endif
}

那么ringbuffer中的数据是怎么来的呢?

这里是采用DMA接收从串口过来的数据并放入ringbufer中,用DMA主要是为了降低CPU负担,如果用串口中断那么K210每发一个字节,立创梁山派就要来处理一次,如果处理的时间稍微长一点就可能导致下一个串口字节来的时候丢失(当然以立创梁山派的240Mhz运行频率来说应该也不会发生这种问题,最需要注意的一点是所有中断都必须快进快出,执行时间要尽可能短)。有了DMA之后就可以收到特定个数字节后再让CPU统一处理。

下面的uart5_recv_buff的大小不是固定不变的,从上面的通讯协议定义我们可以知道小车1的接收字节数是5,小车2的接收字节数是15。依旧是通过CURRENT_AS_CAR1这个宏定义来区分。

// 车1,车2的接收数据大小不一样
#ifdef CURRENT_AS_CAR1
// example:55 01 40 55 00
#define UART5_RECEIVE_LENGTH 5
#else
// example:55 0B C0 5D 00 01 02 00 00 00 00 00 00 00 00
#define UART5_RECEIVE_LENGTH 15
#endif
static uint8_t uart5_recv_buff[UART5_RECEIVE_LENGTH];

关键代码如下:

void uart5_dma_config(void)
{
    dma_single_data_parameter_struct dma_init_struct;
    rcu_periph_clock_enable(UART5_DMA_RCU);

    dma_deinit(UART5_DMA, UART5_DMA_CH);

    dma_init_struct.periph_addr = (uint32_t)&USART_DATA(USART5);
    dma_init_struct.periph_inc = DMA_PERIPH_INCREASE_DISABLE;
    dma_init_struct.memory0_addr = (uint32_t)uart5_recv_buff;
    dma_init_struct.memory_inc = DMA_MEMORY_INCREASE_ENABLE;
    dma_init_struct.periph_memory_width = DMA_PERIPH_WIDTH_8BIT;
    dma_init_struct.circular_mode = DMA_CIRCULAR_MODE_DISABLE;
    dma_init_struct.direction = DMA_PERIPH_TO_MEMORY;
    dma_init_struct.number = UART5_RECEIVE_LENGTH;
    dma_init_struct.priority = DMA_PRIORITY_ULTRA_HIGH;

    dma_single_data_mode_init(UART5_DMA, UART5_DMA_CH, &dma_init_struct);

    dma_channel_subperipheral_select(UART5_DMA, UART5_DMA_CH, DMA_SUBPERI5);

    dma_channel_enable(UART5_DMA, UART5_DMA_CH);

    dma_interrupt_enable(UART5_DMA, UART5_DMA_CH, DMA_CHXCTL_FTFIE);
    nvic_irq_enable(UART5_DMA_CH_IRQ, 1, 1);

    usart_dma_receive_config(USART5, USART_RECEIVE_DMA_ENABLE);

    usart_interrupt_disable(USART5, USART_INT_RBNE);

    usart_flag_clear(USART5, USART_FLAG_IDLE);
    usart_interrupt_enable(USART5, USART_INT_IDLE);
}

void UART5_DMA_CH_IRQ_HANDLER(void)
{
    if (dma_interrupt_flag_get(UART5_DMA, UART5_DMA_CH, DMA_INT_FLAG_FTF)
        == SET)
    {
        dma_interrupt_flag_clear(UART5_DMA, UART5_DMA_CH, DMA_INT_FLAG_FTF);
    }
}


void USART5_IRQHandler(void)
{
    if (usart_interrupt_flag_get(USART5, USART_INT_FLAG_IDLE) == SET)
    {
        usart_data_receive(USART5);

        dma_channel_disable(UART5_DMA, UART5_DMA_CH);
        ring_buffer_write(&rb_uart5, (uint8_t *)&uart5_recv_buff,
                          UART5_RECEIVE_LENGTH);
        uart5_dma_config();

    }

}

当出现串口空闲中断时(串口在一个字节的接收时间后没有再接收到新的数据,就认为蓝牙模块这一帧已经发完了),先关断DMA接收,防止额外的数据造成异常,再调用ring_buffer_write,压到对应ringbuffer内存中,等待读取,最后再重新对串口5的DMA进行配置,开启下一次传输。

为什么要使用ringbuffer在之前的文档中也已经介绍了,这里就不多说了。

3.2.3 在函数中的实际调用

接收部分在主循环中空闲时就会处理,只有从ringbuffer中获取到的数据,就调用upacker进行处理。app_protocol_run();

发送部分由一个软件定时器,定时器3以50ms的间隔,也就是20Hz的频率进行周期性发送。medical_ward_info_timer_callback();

实际代码如下:

/*!
    \brief      main function
    \param[in]  none
    \param[out] none
    \retval     none
*/
int main(void)
{
    board_init();
    bsp_motor_init();
    bsp_encoder_init();
    bsp_led_init();
    bsp_uart_init();
    buzzer_init();
	servo_init();
	
    // 设置SOFT_TIMER_0的超时时间为50ms,重复定时器,给LED闪烁使用
    soft_timer_repeat_init(SOFT_TIMER_0, 50);
    // 设置SOFT_TIMER_1的超时时间为200ms,重复定时器,给PID计算使用
    soft_timer_repeat_init(SOFT_TIMER_1, 20);
    // 设置SOFT_TIMER_3的超时时间为300ms,重复定时器,给小车控制使用
    soft_timer_repeat_init(SOFT_TIMER_2, 1);
    // 设置SOFT_TIMER_4的超时时间为4000ms,重复定时器
    soft_timer_repeat_init(SOFT_TIMER_3, 50);


    while (1)
    {
        // 让梁山派上的四个led灯轮流来回亮
        if (soft_timer_is_timeout(SOFT_TIMER_0))
        {
            bsp_led_left_right_move();
            app_medical_detect_run();
			buzzer_poll();
        }
		// 执行PID计算
        if (soft_timer_is_timeout(SOFT_TIMER_1))
        {
            app_pid_run();
        }
        // 执行小车控制
        if (soft_timer_is_timeout(SOFT_TIMER_2))
        {
            app_car_control_run();

        }
		// ble 信息定时发送,20Hz
        if (soft_timer_is_timeout(SOFT_TIMER_3))
        {
            medical_ward_info_timer_callback();
        }
        app_protocol_run();
    }
}

3.3 控制小车2整体运行的函数

在送药小车的基础部分上开始时间,也是在app_car_control_run函数中运行,通过一个宏定义来区分是小车1还是小车2。

具体理解搭配下面的小车2实现的具体流程图。

下面是对代码控制逻辑的具体解释:

  • 进入初始化状态后首先确保将K210的工作模式切换到识别数字模式,确认当前为数字识别模式后进入下一个状态。

  • 等待K210识别到数字,如果没有识别到数字,k210发送过来的会是0,当获取数字不是0的时候就说明识别到数字了。此时控制K210进入巡红线模式,让蜂鸣器鸣叫一声提醒我们小车可以准备出发了。如果小车1当前位置在c号病房或者d号病房,那么小车2就要执行发挥部分一,切换到 等待小车装载药品状态。否则小车2就要执行发挥部分2,要去远端病房取药,切换到要去远端病房的自选暂停点状态,在这里固定选c号病房为远端的自选暂停点。

  • 等待药物装载,药物装载到之后控制蜂鸣器鸣叫一声提醒我们小车要出发了,并进入第二次判断状态,因为发挥部分不会去近端病房。

  • 第二次判断状态,从药房直走到第二个十字路口,在路口前要进行数字识别,并将获取到的数字都存到药房信息里面,这里对比基础部分又多做了一个处理,如果此时小车1的地图信息已经传过来了,那么小车2就没必要进行数字识别了,直接用小车1的地图数据返回,这样可以节约时间。此时我们就知道了c号病房和d号病房具体的数字了。

    • 如果小车1当前在c号病房,就去d号病房
    • 如果小车1当前在d号病房,就去c号病房
    • 如果都不是,那就也只能硬着头皮去c号病房
  • 小车去c号病房,此时已经到了第二个十字路口,也就是2号位置了,左转90度,直走到病房并点亮黄色指示灯,等待小车1的位置变成到2号位置时,说明小车1开始返回了,此时熄灭黄色指示灯并调转车头,直走到十字路口后再继续直走到d号病房,并点亮绿色指示灯。

  • 小车去d号病房,此时已经到了第二个十字路口,也就是2号位置了,右转90度,直走到病房并点亮黄色指示灯,等待小车1的位置变成到2号位置时,说明小车1开始返回了,此时熄灭黄色指示灯并调转车头,直走到十字路口后再继续直走到c号病房,并点亮绿色指示灯。

  • 去远端病房的自选暂停点,持续等待小车1发出让小车2去取药的指令。如果接收到取药指令,那么小车2去自选暂停点,在这里固定为c号病房,到达c号病房后点亮黄色指示灯,等待小车1的位置变到2号位置时,说明小车1开始返回了,此时熄灭黄色指示灯并去2号位置,左转后切换状态到第三次判断状态

  • 第三次判断状态,直走到3号位置,左转直行后开始数字识别(如果小车2的地图信息传过来的就直接使用小车2的数据信息),将获取到的数字信息存储到药房信息里面,此时我们就知道了e号病房和f号病房的数字标号。

    • 如果e号病房的数字标号和我们开始获取到的数字相等,就去e号病房
    • 如果f号病房的数字标号和我们开始获取到的数字相等,就去f号病房
    • 如果都不是,那就切换到第四次判断状态
  • 小车去e号病房,此时已经到4号位置了,左转90度,直走到病房并点亮红色指示灯,等待取药,流程结束。

  • 小车去f号病房,此时已经到4号位置了,右转90度,直走到病房并点亮红色指示灯,等待取药,流程结束。

  • 第四次判断状态,此时小车已经到了4号位置了,掉头,直走到第二个T字路口(这里直接跳过3号位置的路口识别是因为小车到这里的时候编码器计数值还没到,根本不会判断k210的路口识别结果),也就是要到5号位置并进行数字识别(如果小车1将地图信息传递过来了,那么小车2将不会进行数字识别,直接使用小车1传递过来的地图信息)。将获取到的数字存到药房信息里面,此时我们就知道了g号病房和h号病房的具体数字了。

    • 如果g号病房的数字标号和我们开始获取到的数字相等,就去g号病房
    • 如果h号病房的数字标号和我们开始获取到的数字相等,就去h号病房
    • 如果都不是,那也没办法了,只能蒙一个去g号病房了。
  • 小车去g号病房,此时小车已经到了5号位置了,左转90度,直走到病房并点亮红色指示灯,等待取药,流程结束。

  • 小车去h号病房,此时小车已经到了5号位置了,右转90度,直走到病房并点亮红色指示灯,等待取药,流程结束。

4 小车2实现的具体流程图

小车2流程图

马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
C
1
https://gitee.com/lcsc/public_bare_medical_car.git
git@gitee.com:lcsc/public_bare_medical_car.git
lcsc
public_bare_medical_car
裸机版基于立创梁山派的21年电赛F题智能送药小车
master

搜索帮助