本人使用的设备/驱动:
- Windows10
- 串口助手 4.3.25(其实啥都行)
- 桃饱随处可买的u***-ttl(ch340G)
- 桃饱随处可买的stlink
- mpu6050(一个板载,一个通过I2C接插件连接外置)
- cubeMX 5.6.1
- PlatformIO
- pycharm community2019
- stm32f103rct6的storm32BGC
使用stm32f103rct6,配置uart中断,配合pyqt在线调参
本文直接从项目中加入相关功能说起,环境以及其他外设配置:
CubeMX配合PlatformIO开发STM32,
CubeMX配合PlatformIO开发STM32,
CubeMX配合PlatformIO开发STM32,
CubeMX配合PlatformIO开发STM32,
CubeMX配合PlatformIO开发STM32,
CubeMX配合PlatformIO开发STM32,
CubeMX配合PlatformIO开发STM32,
CubeMX配合PlatformIO开发STM32,
CubeMX配合PlatformIO开发STM32,
每改一次pid都要重新烧太麻烦了,能在线调多美好啊
这是设计好的gui,核心部分是上面的滑轨以及右边显示的两列lcd,左边一列lcd是实时lcd,反应滑轨;右边一列lcd是lcd_saved,显示storm开发板中pid的参数。
大概的交互逻辑大致如下:
首先搞定显示的问题:
确定模块的功能设置:
class GuiController(QWidget):
def initUI(self):
##################参数P调整#########################
self.PLcd_text = QLCDNumber(self)
self.PLcd_text.setSegmentStyle(QLCDNumber.Filled)
self.PLcd_text.display('P')#固定显示“P”这个字符
self.PSlider = QSlider(Qt.Horizontal, self)
self.PSlider.setFocusPolicy(Qt.NoFocus)
self.PSlider.setMinimum(0)#设置滑轨最小值以及最大值
self.PSlider.setMaximum(300)
self.PSlider.setSingleStep(2)#滑轨的调整间隔
self.PSlider.setTickPosition(QSlider.TicksBelow)
self.PSlider.valueChanged.connect(self.PChange_temp)#与一个函数PChange_temp连接,在滑轨值变化时调用这个函数
self.PLcd = QLCDNumber(self) #设置一个lcd,PChange_temp的改变反应到这上面来
self.PLcd.setSegmentStyle(QLCDNumber.Flat)
self.PLcd.setSmallDecimalPoint(True)
self.PLcd.setDigitCount(3)
##################参数I调整#########################
self.ILcd_text = QLCDNumber(self)
self.ILcd_text.setSegmentStyle(QLCDNumber.Filled)
self.ILcd_text.display(1)
self.ISlider = QSlider(Qt.Horizontal, self)
self.ISlider.setFocusPolicy(Qt.NoFocus)
self.ISlider.setMinimum(0)
self.ISlider.setMaximum(300)
self.ISlider.setSingleStep(2)
self.ISlider.setTickPosition(QSlider.TicksBelow)
self.ISlider.valueChanged.connect(self.IChange_temp)
self.ILcd = QLCDNumber(self)
self.ILcd.setSegmentStyle(QLCDNumber.Flat)
self.ILcd.setSmallDecimalPoint(True)
self.ILcd.setDigitCount(3)
##################参数D调整#########################
self.DLcd_text = QLCDNumber(self)
self.DLcd_text.setSegmentStyle(QLCDNumber.Filled)
self.DLcd_text.display(0)
self.DSlider = QSlider(Qt.Horizontal, self)
self.DSlider.setFocusPolicy(Qt.NoFocus)
self.DSlider.setMinimum(0)
self.DSlider.setMaximum(300)
self.DSlider.setSingleStep(2)
self.DSlider.setTickPosition(QSlider.TicksBelow)
self.DSlider.valueChanged.connect(self.DChange_temp)
self.DLcd = QLCDNumber(self)
self.DLcd.setSegmentStyle(QLCDNumber.Flat)
self.DLcd.setSmallDecimalPoint(True)
self.DLcd.setDigitCount(3)
self.Select_motor =QComboBox(self) #下拉菜单
self.Select_motor.addItem('Pitch')
self.Select_motor.addItem('Roll')
self.Select_motor.addItem('Yaw')
# self.Select_motor.setCurrentIndex('Pitch')
self.Select_motor.currentIndexChanged[str].connect(self.motor_selected) # 条目发生改变,发射信号,传递条目内容
self.SetPIDButton = QPushButton("SetPID") #设置PID,将pid传入bgc,并显示到最右栏
self.SetPIDButton.clicked.connect(self.SetPIDCB)
这些功能都只在class中相互调用:如Xlcd显示XSlider的值;而上面说到的lcd_saved(写入板子的PID值)需要被多个class调用,如读到串口数据要更改,按下按钮“setPID”也要更改,所以写在代码开头,方便被各个class调用
#因为要跨类调用,所以定义在函数外部作全局用
PLcd_saved = QLCDNumber()
PLcd_saved.setSegmentStyle(QLCDNumber.Flat)
PLcd_saved.setSmallDecimalPoint(True)
PLcd_saved.setDigitCount(3)
ILcd_saved = QLCDNumber()
ILcd_saved.setSegmentStyle(QLCDNumber.Flat)
ILcd_saved.setSmallDecimalPoint(True)
ILcd_saved.setDigitCount(3)
DLcd_saved = QLCDNumber()
DLcd_saved.setSegmentStyle(QLCDNumber.Flat)
DLcd_saved.setSmallDecimalPoint(True)
DLcd_saved.setDigitCount(3)
回到gui的class下,在grid形式下编程:确定模块的位置
self.grid.addWidget(self.PLcd_text, 2, 0)#显示“P”字符
self.grid.addWidget(self.PSlider, 2, 1, 1, 1)#滑轨
self.grid.addWidget(self.PLcd, 2, 2)#滑轨上的参数
self.grid.addWidget(PLcd_saved, 2, 3)#已保存到bgc的
self.grid.addWidget(self.ILcd_text, 3, 0)
self.grid.addWidget(self.ISlider, 3, 1, 1, 1)
self.grid.addWidget(self.ILcd, 3, 2)
self.grid.addWidget(ILcd_saved, 3, 3)
self.grid.addWidget(self.DLcd_text, 4, 0)
self.grid.addWidget(self.DSlider, 4, 1, 1, 1)
self.grid.addWidget(self.DLcd, 4, 2)
self.grid.addWidget(DLcd_saved, 4, 3)
self.grid.addWidget(self.Select_motor, 5, 1)
self.grid.addWidget(self.SetPIDButton, 5, 0)
各个模块调用的功能函数,包括实时显示滑轨,下拉菜单选择的电机,按下setPID按钮的反应
def PChange_temp(self):
value = float(self.PSlider.value())#获取滑轨的值
print("P:", value)
self.PLcd.display(value)#在lcd中显示
def IChange_temp(self):
value = float(self.ISlider.value())
print("I:", value)
self.ILcd.display(value)
def DChange_temp(self):
value = float(self.DSlider.value())
print("D:", value)
self.DLcd.display(value)
def motor_selected(self):
global motor_num
value = self.Select_motor.currentText()#当前下拉栏中选择的文字。注意,刚初始化的没有选择过的下拉列表value读不到值
if value == "Pitch":
motor_num = 0
elif value == "Roll":
motor_num = 1
else:
motor_num = 2
def SetPIDCB(self): #将PID传输出去,并更改显示
global P,I,D
global origin_pid_flag
global set_PID_flag
if origin_pid_flag==1:#如果是开机读到的pid数据
PLcd_saved.display(origin_P)
ILcd_saved.display(origin_I)
DLcd_saved.display(origin_D)
origin_pid_flag=0
else:
P = float(self.PSlider.value())#将滑轨数据保存下来
I = float(self.ISlider.value())
D = float(self.DSlider.value())
set_PID_flag = 1 #使能向bgc的发送,防止乱跳
新建一个class,完成串口发送以及确认的功能。如果发送的数据的返回值跟发送值不同,则不停重发,有点暴力的协议但是实际还是有用的。
class sendThread(QThread): #send bytes
def __init__(self):
super().__init__()
def run(self):
global P, I, D
global conn_flag, set_PID_flag
global write_success_flag
while True:
time.sleep(0.01)
if (conn_flag == 0) & (set_PID_flag == 1):#当串口连接以及按下写入按钮后执行
# data = [int(0), float(vmax),
# float(angle), float(0.0)]
while write_success_flag==0: #确定是否写入成功
data = [int(motor_num), float(P),
float(I), float(D)]
serial.writeData(data) #这是一个自己写的函数,通过串口发送数据,顺序为time yaw roll pitch
print("write:", motor_num, "P:", P, "I:", I, "D:", D)
time.sleep(0.005)
print("Motor:", motor_num, "P:", P, "I:", I, "D:", D)
PLcd_saved.display(P) #显示到最右侧的LCD上去
ILcd_saved.display(I)
DLcd_saved.display(D)
write_success_flag=0
set_PID_flag=0
注意,因为gui中需要这个功能,所以还需要在guicontroller的类下:
def __init__(self, length):
super().__init__()
self.sendThread = sendThread()
接下来进入pio中,首先使能usart3的中断
允许usart接受中断:
HAL_UART_Receive_IT(uart3_it.uart,&(uart3_it.tmp_buff),1);
在gui发来数据后触发中断,进入回调函数:
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART3)
{
if (uart3_it.rx_bytes_count >= 18)//judge whether length is matched
{
uart3_it.rx_bytes_count = 0;
memset(&(uart3_it.data), 0, sizeof(Serial_Receive_Stream_typeDef));
uart3_it.is_compelete = 0;
}
else
{
uint8_t *ptr = (uint8_t *)(&(uart3_it.data));
ptr[uart3_it.rx_bytes_count++] = uart3_it.tmp_buff;//read buffer
if (ptr[uart3_it.rx_bytes_count - 2] == 'r' && ptr[uart3_it.rx_bytes_count - 1] == 'n')//last 2bytes
{
if(uart3_it.rx_bytes_count==18){//if length is matched,then enable a flag which means data is available
uart3_it.is_compelete = 1;
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_13);
uart3_it.rx_bytes_count = 0;
}else{
uart3_it.rx_bytes_count = 0;
memset(&(uart3_it.data), 0, sizeof(Serial_Receive_Stream_typeDef));
uart3_it.is_compelete = 0;
}
}
else
{
uart3_it.is_compelete = 0;
HAL_UART_Receive_IT(uart3_it.uart, &(uart3_it.tmp_buff), 1);// enable interrupt
}
}
}
}
主函数中根据传输成功的标志位进入函数:
if(uart3_it.is_compelete){
uart3_it.is_compelete = 0;
// updateEncoder(&roll_encoder);
motor_num= uart3_it.data.time_stamp;
if(sendpid(motor_num,&uart3_it.data.yaw,&uart3_it.data.roll,&uart3_it.data.pitch))
{
serial_data.time_stamp = uart3_it.data.time_stamp;
serial_data.yaw = uart3_it.data.yaw;
serial_data.roll = uart3_it.data.roll;
serial_data.pitch = uart3_it.data.pitch;
SerialPrintTransmit(&serial_data);
printf("transmited_to_clientrn");
}
printf("transmited_to_clientrn");
HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_12);
}
给PID赋值(通过返回标志位表示完成赋值)
/* USER CODE BEGIN 4 */
int sendpid(int motornum,float *get_P, float *get_I, float *get_D)
{
static int PID_transmit_complete=0;
PID_transmit_complete=0;
if(motornum==0)
{
pitchPPara=*get_P;
pitchIPara=*get_I;
pitchDPara=*get_D;
printf("storm_pitch:P:%f,I:%f,D:%frn",pitchPPara,pitchIPara,pitchDPara);
}
else if(motornum==1)
{
rollPPara=*get_P;
rollIPara=*get_I;
rollDPara=*get_D;
printf("storm_roll:P:%f,I:%f,D:%frn",rollPPara,rollIPara,rollDPara);
}
else
{
yawPPara=*get_P;
yawIPara=*get_I;
yawDPara=*get_D;
printf("storm_yaw:P:%f,I:%f,D:%frn",yawPPara,yawIPara,yawDPara);
}
PID_transmit_complete=1;
return PID_transmit_complete;
}
/* USER CODE END 4 */
通过串口发回去后,gui会判断是否停止发送。
至此,逻辑闭环
本人使用的设备/驱动:
- Windows10
- 串口助手 4.3.25(其实啥都行)
- 桃饱随处可买的u***-ttl(ch340G)
- 桃饱随处可买的stlink
- mpu6050(一个板载,一个通过I2C接插件连接外置)
- cubeMX 5.6.1
- PlatformIO
- pycharm community2019
- stm32f103rct6的storm32BGC
使用stm32f103rct6,配置uart中断,配合pyqt在线调参
本文直接从项目中加入相关功能说起,环境以及其他外设配置:
CubeMX配合PlatformIO开发STM32,
CubeMX配合PlatformIO开发STM32,
CubeMX配合PlatformIO开发STM32,
CubeMX配合PlatformIO开发STM32,
CubeMX配合PlatformIO开发STM32,
CubeMX配合PlatformIO开发STM32,
CubeMX配合PlatformIO开发STM32,
CubeMX配合PlatformIO开发STM32,
CubeMX配合PlatformIO开发STM32,
每改一次pid都要重新烧太麻烦了,能在线调多美好啊
这是设计好的gui,核心部分是上面的滑轨以及右边显示的两列lcd,左边一列lcd是实时lcd,反应滑轨;右边一列lcd是lcd_saved,显示storm开发板中pid的参数。
大概的交互逻辑大致如下:
首先搞定显示的问题:
确定模块的功能设置:
class GuiController(QWidget):
def initUI(self):
##################参数P调整#########################
self.PLcd_text = QLCDNumber(self)
self.PLcd_text.setSegmentStyle(QLCDNumber.Filled)
self.PLcd_text.display('P')#固定显示“P”这个字符
self.PSlider = QSlider(Qt.Horizontal, self)
self.PSlider.setFocusPolicy(Qt.NoFocus)
self.PSlider.setMinimum(0)#设置滑轨最小值以及最大值
self.PSlider.setMaximum(300)
self.PSlider.setSingleStep(2)#滑轨的调整间隔
self.PSlider.setTickPosition(QSlider.TicksBelow)
self.PSlider.valueChanged.connect(self.PChange_temp)#与一个函数PChange_temp连接,在滑轨值变化时调用这个函数
self.PLcd = QLCDNumber(self) #设置一个lcd,PChange_temp的改变反应到这上面来
self.PLcd.setSegmentStyle(QLCDNumber.Flat)
self.PLcd.setSmallDecimalPoint(True)
self.PLcd.setDigitCount(3)
##################参数I调整#########################
self.ILcd_text = QLCDNumber(self)
self.ILcd_text.setSegmentStyle(QLCDNumber.Filled)
self.ILcd_text.display(1)
self.ISlider = QSlider(Qt.Horizontal, self)
self.ISlider.setFocusPolicy(Qt.NoFocus)
self.ISlider.setMinimum(0)
self.ISlider.setMaximum(300)
self.ISlider.setSingleStep(2)
self.ISlider.setTickPosition(QSlider.TicksBelow)
self.ISlider.valueChanged.connect(self.IChange_temp)
self.ILcd = QLCDNumber(self)
self.ILcd.setSegmentStyle(QLCDNumber.Flat)
self.ILcd.setSmallDecimalPoint(True)
self.ILcd.setDigitCount(3)
##################参数D调整#########################
self.DLcd_text = QLCDNumber(self)
self.DLcd_text.setSegmentStyle(QLCDNumber.Filled)
self.DLcd_text.display(0)
self.DSlider = QSlider(Qt.Horizontal, self)
self.DSlider.setFocusPolicy(Qt.NoFocus)
self.DSlider.setMinimum(0)
self.DSlider.setMaximum(300)
self.DSlider.setSingleStep(2)
self.DSlider.setTickPosition(QSlider.TicksBelow)
self.DSlider.valueChanged.connect(self.DChange_temp)
self.DLcd = QLCDNumber(self)
self.DLcd.setSegmentStyle(QLCDNumber.Flat)
self.DLcd.setSmallDecimalPoint(True)
self.DLcd.setDigitCount(3)
self.Select_motor =QComboBox(self) #下拉菜单
self.Select_motor.addItem('Pitch')
self.Select_motor.addItem('Roll')
self.Select_motor.addItem('Yaw')
# self.Select_motor.setCurrentIndex('Pitch')
self.Select_motor.currentIndexChanged[str].connect(self.motor_selected) # 条目发生改变,发射信号,传递条目内容
self.SetPIDButton = QPushButton("SetPID") #设置PID,将pid传入bgc,并显示到最右栏
self.SetPIDButton.clicked.connect(self.SetPIDCB)
这些功能都只在class中相互调用:如Xlcd显示XSlider的值;而上面说到的lcd_saved(写入板子的PID值)需要被多个class调用,如读到串口数据要更改,按下按钮“setPID”也要更改,所以写在代码开头,方便被各个class调用
#因为要跨类调用,所以定义在函数外部作全局用
PLcd_saved = QLCDNumber()
PLcd_saved.setSegmentStyle(QLCDNumber.Flat)
PLcd_saved.setSmallDecimalPoint(True)
PLcd_saved.setDigitCount(3)
ILcd_saved = QLCDNumber()
ILcd_saved.setSegmentStyle(QLCDNumber.Flat)
ILcd_saved.setSmallDecimalPoint(True)
ILcd_saved.setDigitCount(3)
DLcd_saved = QLCDNumber()
DLcd_saved.setSegmentStyle(QLCDNumber.Flat)
DLcd_saved.setSmallDecimalPoint(True)
DLcd_saved.setDigitCount(3)
回到gui的class下,在grid形式下编程:确定模块的位置
self.grid.addWidget(self.PLcd_text, 2, 0)#显示“P”字符
self.grid.addWidget(self.PSlider, 2, 1, 1, 1)#滑轨
self.grid.addWidget(self.PLcd, 2, 2)#滑轨上的参数
self.grid.addWidget(PLcd_saved, 2, 3)#已保存到bgc的
self.grid.addWidget(self.ILcd_text, 3, 0)
self.grid.addWidget(self.ISlider, 3, 1, 1, 1)
self.grid.addWidget(self.ILcd, 3, 2)
self.grid.addWidget(ILcd_saved, 3, 3)
self.grid.addWidget(self.DLcd_text, 4, 0)
self.grid.addWidget(self.DSlider, 4, 1, 1, 1)
self.grid.addWidget(self.DLcd, 4, 2)
self.grid.addWidget(DLcd_saved, 4, 3)
self.grid.addWidget(self.Select_motor, 5, 1)
self.grid.addWidget(self.SetPIDButton, 5, 0)
各个模块调用的功能函数,包括实时显示滑轨,下拉菜单选择的电机,按下setPID按钮的反应
def PChange_temp(self):
value = float(self.PSlider.value())#获取滑轨的值
print("P:", value)
self.PLcd.display(value)#在lcd中显示
def IChange_temp(self):
value = float(self.ISlider.value())
print("I:", value)
self.ILcd.display(value)
def DChange_temp(self):
value = float(self.DSlider.value())
print("D:", value)
self.DLcd.display(value)
def motor_selected(self):
global motor_num
value = self.Select_motor.currentText()#当前下拉栏中选择的文字。注意,刚初始化的没有选择过的下拉列表value读不到值
if value == "Pitch":
motor_num = 0
elif value == "Roll":
motor_num = 1
else:
motor_num = 2
def SetPIDCB(self): #将PID传输出去,并更改显示
global P,I,D
global origin_pid_flag
global set_PID_flag
if origin_pid_flag==1:#如果是开机读到的pid数据
PLcd_saved.display(origin_P)
ILcd_saved.display(origin_I)
DLcd_saved.display(origin_D)
origin_pid_flag=0
else:
P = float(self.PSlider.value())#将滑轨数据保存下来
I = float(self.ISlider.value())
D = float(self.DSlider.value())
set_PID_flag = 1 #使能向bgc的发送,防止乱跳
新建一个class,完成串口发送以及确认的功能。如果发送的数据的返回值跟发送值不同,则不停重发,有点暴力的协议但是实际还是有用的。
class sendThread(QThread): #send bytes
def __init__(self):
super().__init__()
def run(self):
global P, I, D
global conn_flag, set_PID_flag
global write_success_flag
while True:
time.sleep(0.01)
if (conn_flag == 0) & (set_PID_flag == 1):#当串口连接以及按下写入按钮后执行
# data = [int(0), float(vmax),
# float(angle), float(0.0)]
while write_success_flag==0: #确定是否写入成功
data = [int(motor_num), float(P),
float(I), float(D)]
serial.writeData(data) #这是一个自己写的函数,通过串口发送数据,顺序为time yaw roll pitch
print("write:", motor_num, "P:", P, "I:", I, "D:", D)
time.sleep(0.005)
print("Motor:", motor_num, "P:", P, "I:", I, "D:", D)
PLcd_saved.display(P) #显示到最右侧的LCD上去
ILcd_saved.display(I)
DLcd_saved.display(D)
write_success_flag=0
set_PID_flag=0
注意,因为gui中需要这个功能,所以还需要在guicontroller的类下:
def __init__(self, length):
super().__init__()
self.sendThread = sendThread()
接下来进入pio中,首先使能usart3的中断
允许usart接受中断:
HAL_UART_Receive_IT(uart3_it.uart,&(uart3_it.tmp_buff),1);
在gui发来数据后触发中断,进入回调函数:
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART3)
{
if (uart3_it.rx_bytes_count >= 18)//judge whether length is matched
{
uart3_it.rx_bytes_count = 0;
memset(&(uart3_it.data), 0, sizeof(Serial_Receive_Stream_typeDef));
uart3_it.is_compelete = 0;
}
else
{
uint8_t *ptr = (uint8_t *)(&(uart3_it.data));
ptr[uart3_it.rx_bytes_count++] = uart3_it.tmp_buff;//read buffer
if (ptr[uart3_it.rx_bytes_count - 2] == 'r' && ptr[uart3_it.rx_bytes_count - 1] == 'n')//last 2bytes
{
if(uart3_it.rx_bytes_count==18){//if length is matched,then enable a flag which means data is available
uart3_it.is_compelete = 1;
HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_13);
uart3_it.rx_bytes_count = 0;
}else{
uart3_it.rx_bytes_count = 0;
memset(&(uart3_it.data), 0, sizeof(Serial_Receive_Stream_typeDef));
uart3_it.is_compelete = 0;
}
}
else
{
uart3_it.is_compelete = 0;
HAL_UART_Receive_IT(uart3_it.uart, &(uart3_it.tmp_buff), 1);// enable interrupt
}
}
}
}
主函数中根据传输成功的标志位进入函数:
if(uart3_it.is_compelete){
uart3_it.is_compelete = 0;
// updateEncoder(&roll_encoder);
motor_num= uart3_it.data.time_stamp;
if(sendpid(motor_num,&uart3_it.data.yaw,&uart3_it.data.roll,&uart3_it.data.pitch))
{
serial_data.time_stamp = uart3_it.data.time_stamp;
serial_data.yaw = uart3_it.data.yaw;
serial_data.roll = uart3_it.data.roll;
serial_data.pitch = uart3_it.data.pitch;
SerialPrintTransmit(&serial_data);
printf("transmited_to_clientrn");
}
printf("transmited_to_clientrn");
HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_12);
}
给PID赋值(通过返回标志位表示完成赋值)
/* USER CODE BEGIN 4 */
int sendpid(int motornum,float *get_P, float *get_I, float *get_D)
{
static int PID_transmit_complete=0;
PID_transmit_complete=0;
if(motornum==0)
{
pitchPPara=*get_P;
pitchIPara=*get_I;
pitchDPara=*get_D;
printf("storm_pitch:P:%f,I:%f,D:%frn",pitchPPara,pitchIPara,pitchDPara);
}
else if(motornum==1)
{
rollPPara=*get_P;
rollIPara=*get_I;
rollDPara=*get_D;
printf("storm_roll:P:%f,I:%f,D:%frn",rollPPara,rollIPara,rollDPara);
}
else
{
yawPPara=*get_P;
yawIPara=*get_I;
yawDPara=*get_D;
printf("storm_yaw:P:%f,I:%f,D:%frn",yawPPara,yawIPara,yawDPara);
}
PID_transmit_complete=1;
return PID_transmit_complete;
}
/* USER CODE END 4 */
通过串口发回去后,gui会判断是否停止发送。
至此,逻辑闭环
举报