打开工程9_car_advance
,这个工程是送药小车题目中的基本要求实现,有关控制小车运动的函数都在app_car_control.c
文件中,在app_car_control_run(void)
函数中,通过一个在car_config.h
文件中的宏定义#define CURRENT_AS_CAR1
来确定这个工程是小车1的工程还是小车2的工程。就是说这个宏定义存在那么这个工程就是小车1,这个宏定义不存在那么这个工程就是小车2。让一个工程同时支持两个固件,方便后面维护。
在送药小车基础部分的实现中,我们已经能让小车在地图中顺利运动了,也可以实现基础部分的要求了。可以看到在上面的要求中,最重要的就是两个小车要可以互相通讯,小车2要知道小车1的位置才能正确选择自选暂停点。小车1要能给小车2发送取药指令。
在这里我用的是国产沁恒的CH9143
蓝牙串口USB三向通讯模块。他的主要优点有两个:
在 **立创梁山派与K210串口通讯协议框架搭建 **中,我们已经知道了为什么要是用定义好的通讯协议,解决了如何解决串口通讯的粘包,分包,校验问题,实现我们所定义的通讯协议。在这两个小车的双向通讯的中,我们也是要将这些问题解决。
有关的实现都在app_protocol.c
文件中,小车1的发送数据就是小车2的接收数据,小车1的接收数据就是小车2的发送数据。
发送
负载包 | 变量名 | 数据所对应的意义 |
---|---|---|
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 |
发送
负载包 | 含义 | 数据所对应的意义 |
---|---|---|
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去取药。 |
这个的处理和 立创梁山派与K210串口通讯协议框架搭建 中一样,使用国产RT-Thread的软件包Upacker,具体可以看那一篇文档,这里就不过多介绍upacker和ringbuffer了。
引入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
......
/**
* @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_info
或car2_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_info
和get_car1_to_car2_info
也是将upacker解析成功后的数据定时刷新,供给小车控制使用。
简单来说,送药小车接收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在之前的文档中也已经介绍了,这里就不多说了。
接收部分在主循环中空闲时就会处理,只有从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();
}
}
在送药小车的基础部分上开始时间,也是在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号病房具体的数字了。
小车去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号病房,此时已经到4号位置了,左转90度,直走到病房并点亮红色指示灯,等待取药,流程结束。
小车去f号病房,此时已经到4号位置了,右转90度,直走到病房并点亮红色指示灯,等待取药,流程结束。
第四次判断状态,此时小车已经到了4号位置了,掉头,直走到第二个T字路口(这里直接跳过3号位置的路口识别是因为小车到这里的时候编码器计数值还没到,根本不会判断k210的路口识别结果),也就是要到5号位置并进行数字识别(如果小车1将地图信息传递过来了,那么小车2将不会进行数字识别,直接使用小车1传递过来的地图信息)。将获取到的数字存到药房信息里面,此时我们就知道了g号病房和h号病房的具体数字了。
小车去g号病房,此时小车已经到了5号位置了,左转90度,直走到病房并点亮红色指示灯,等待取药,流程结束。
小车去h号病房,此时小车已经到了5号位置了,右转90度,直走到病房并点亮红色指示灯,等待取药,流程结束。
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。