STM32 CAN通信(没有can芯片多机通信)

前言

只有一块stm32单片机或两块的情况下,如何调试stm32 can通信,请见上文
链接: 单机回环与双机通信教程

之后又小伙伴评论说多机通信调试不同,有的单片机容易卡死,不知道是什么原因,我猜测可能是:

  • id配置问题,id代表了优先级,如果总线总是被优先级高的子模块占用,那么其他模块可能会卡死
  • 波特率配置问题,多个can在一根总线上时,必须使用相同波特率

总而言之,很可能是配置问题,于是我又翻出了一块stm32核心板,3个核心板用can组网通信。

硬件

简单解释一下总线需求:

  • 假设同时有2台或以上设备连接到总线
  • 都不发信号时,总线上逻辑为1(忽略逻辑电路与差分电路区别,逻辑电路中为高电平,差分电路中为隐性)
  • 只要有任何一个设备发送0,总线就会变成0(线与)
  • 突然两台设备同时发送数据,A设备发送01100111,B设备发送01110111
  • 我们逐位分析,第一位两个设备都发0,发送的同时设备会读取总线,发现总线的确是0,继续发送第二位。
  • 第二、三位数据都是1,读取到的总线也是1,继续
  • 第四位数据,A发送0,B发送1,总线为0(线与)。A设备读取总线为0,继续发送;B设备读取总线数据为0,一脸懵逼,发现和自己发送的数据1不一样,于是停止发送,改为接收数据。
  • A发送第五位数据,B接收数据

原理图
在这里插入图片描述

原理图中总线可以实现:

  • 如果都不发数据,总线相当于直接接在3.3V高电平上,也就是逻辑1。
  • 当AB同时发送数据,A_CAN_TX发0,B_CAN_TX发1。此时,A的二极管导通,总线上电压约0.5V(二极管压降为0.5V,stm32单片机手册写了,0至0.7V为低电平)。而B的二极管不能导通。A读取总线为逻辑0,与自身数据相同,继续发送。B读取总线数据为逻辑0,与自身不符,停止发送数据,转为接收数据。也就是实现线与。

实物图如下:
在这里插入图片描述

软件

程序

连接好三块板子电源后(如果不是使用同一个电源供电,需要共地),烧录程序如下,分别打开 #define A#define B#define C宏定义,烧录到三个单片机中即可

#include "stm32f10x.h"
#include "stdio.h"

#define A
//#define B
//#define C

#ifdef A
#define my_id		0x00000001
#define other_id	0x00000002
#define my_dat0		0x00
#define my_dat1		0x00
#define my_dat2		0x00
#define my_dat3		0x00
#define my_dat4		0x00
#define my_dat5		0x00
#define my_dat6		0x00
#define my_dat7		0x01
#endif

#ifdef B
#define my_id		0x00000002
#define other_id	0x00000003
#define my_dat0		0x00
#define my_dat1		0x00
#define my_dat2		0x00
#define my_dat3		0x00
#define my_dat4		0x00
#define my_dat5		0x00
#define my_dat6		0x00
#define my_dat7		0x02
#endif

#ifdef C
#define my_id		0x00000003
#define other_id	0x00000001
#define my_dat0		0x00
#define my_dat1		0x00
#define my_dat2		0x00
#define my_dat3		0x00
#define my_dat4		0x00
#define my_dat5		0x00
#define my_dat6		0x00
#define my_dat7		0x03
#endif

CanTxMsg TxMessage;			//can发送消息结构体
CanRxMsg RxMessage;			//can接收消息结构体
uint8_t ledstatu;			//led状态
uint8_t flag;				//接收到数据标志

void DelayUs(uint32_t us);	//延时1us
void DelayMs(uint32_t ms);	//延时1ms

void LED_Config(void);		//led配置			C13
void USART1_Config(void);	//USART1配置		UART_TX A9	UART_RX A10
void CAN1_Config(void);		//CAN1配置			CAN_TX	B9	CAN_RX B8

void CAN_SetMsg(CanTxMsg *TxMessage);			//设置发送消息内容
void Init_RxMes(CanRxMsg *RxMessage);			//清空接收消息内容
void USB_LP_CAN1_RX0_IRQHandler(void);			//接收中断

//接收中断,收到数据时触发这个函数
void USB_LP_CAN1_RX0_IRQHandler(void)
{
	/*从邮箱中读出报文*/
	CAN_Receive(CAN1, CAN_FIFO0, &RxMessage);

	flag = 1; 					       //接收成功  
}

int main(void)
{		
	int8_t i;
	
	LED_Config();
	USART1_Config();
	CAN1_Config();
	CAN_SetMsg(&TxMessage);
	Init_RxMes(&RxMessage);
	
	printf("\r\n 扩展ID号ExtId:0x%x \r\n",TxMessage.ExtId);
		
	while (1)
	{
		static uint32_t u32Time100ms = 0;		//100ms 计数一次
		
		if(u32Time100ms%10 == 0)//1s定时
		{
			//闪灯
			GPIO_WriteBit(GPIOC,GPIO_Pin_13,(BitAction)ledstatu);
			ledstatu=!ledstatu;
			
			//发数据
			CAN_Transmit(CAN1, &TxMessage);
			
			printf("0x%08x发送:\n",my_id);
			for(i=0;i<8;i++)
				printf("%02x ",TxMessage.Data[i]);
			printf("\n");
		}
		
		if(flag==1)//接收到数据
		{
			printf("接收0x%08x:\n",RxMessage.ExtId);
			for(i=0;i<8;i++)
				printf("%02x ",RxMessage.Data[i]);
			printf("\n");
			
			flag=0;
		}
		
		DelayMs(100);
		u32Time100ms++;
	}
}

void DelayUs(uint32_t us)
{
	while(us--)
	{
		__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();
		__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();
		__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();
		__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();
		__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();
		__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();
		__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();__NOP();
	}
	
}
void DelayMs(uint32_t ms)
{
	while(ms--)
		DelayUs(1000);
}

void LED_Config(void)
{		
		GPIO_InitTypeDef GPIO_InitStructure;

		RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOC, ENABLE);
		GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;	
		GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;   
		GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; 
		GPIO_Init(GPIOC, &GPIO_InitStructure);	
}

void USART1_Config(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;
	USART_InitTypeDef USART_InitStructure;

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);

	// 将USART Tx的GPIO配置为推挽复用模式
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);

	// 将USART Rx的GPIO配置为浮空输入模式
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	// 配置串口的工作参数
	USART_InitStructure.USART_BaudRate = 115200;
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;
	USART_InitStructure.USART_StopBits = USART_StopBits_1;
	USART_InitStructure.USART_Parity = USART_Parity_No ;
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
	USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
	USART_Init(USART1, &USART_InitStructure);
	
	// 使能串口
	USART_Cmd(USART1, ENABLE);
}

///重定向c库函数printf到串口,重定向后可使用printf函数
int fputc(int ch, FILE *f)
{
	/* 发送一个字节数据到串口 */
	USART_SendData(USART1, (uint8_t) ch);

	/* 等待发送完毕 */
	while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);		
	return (ch);
}






void CAN1_Config(void)
{
 	GPIO_InitTypeDef GPIO_InitStructure;   	
	NVIC_InitTypeDef NVIC_InitStructure;
	CAN_InitTypeDef        CAN_InitStructure;
	CAN_FilterInitTypeDef  CAN_FilterInitStructure;

	//引脚配置************************************************************************
	/* Enable GPIO clock */
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO|RCC_APB2Periph_GPIOB, ENABLE);

	//重映射引脚
	GPIO_PinRemapConfig(GPIO_Remap1_CAN1, ENABLE);

	/* Configure CAN TX pins */
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;		         // 复用推挽输出
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);

	/* Configure CAN RX  pins */
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 ;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;	             // 上拉输入
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);



	//中断配置************************************************************************
	/* Configure one bit for preemption priority */
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
	
	/*中断设置*/
	NVIC_InitStructure.NVIC_IRQChannel = USB_LP_CAN1_RX0_IRQn;	   //CAN1 RX0中断
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;		   //抢占优先级0
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;			   //子优先级为0
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStructure);
	
	
	//通信参数配置********************************************************************
	/* Enable CAN clock */
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);

	/*CAN寄存器初始化*/
	CAN_DeInit(CAN1);
	CAN_StructInit(&CAN_InitStructure);

	/*CAN单元初始化*/
	CAN_InitStructure.CAN_TTCM=DISABLE;			   //MCR-TTCM  关闭时间触发通信模式使能
	CAN_InitStructure.CAN_ABOM=ENABLE;			   //MCR-ABOM  自动离线管理 
	CAN_InitStructure.CAN_AWUM=ENABLE;			   //MCR-AWUM  使用自动唤醒模式
	CAN_InitStructure.CAN_NART=DISABLE;			   //MCR-NART  禁止报文自动重传	  DISABLE-自动重传
	CAN_InitStructure.CAN_RFLM=DISABLE;			   //MCR-RFLM  接收FIFO 锁定模式  DISABLE-溢出时新报文会覆盖原有报文  
	CAN_InitStructure.CAN_TXFP=DISABLE;			   //MCR-TXFP  发送FIFO优先级 DISABLE-优先级取决于报文标示符 
	CAN_InitStructure.CAN_Mode = CAN_Mode_Normal;  //普通收发模式 CAN_Mode_Normal 、 回环工作模式 CAN_Mode_LoopBack
	CAN_InitStructure.CAN_SJW=CAN_SJW_1tq;		   //BTR-SJW 重新同步跳跃宽度 1个时间单元

	/* ss=1 bs1=5 bs2=3 位时间宽度为(1+5+3) 波特率即为时钟周期tq*(1+3+5)  */
	CAN_InitStructure.CAN_BS1=CAN_BS1_5tq;		   //BTR-TS1 时间段1 占用了5个时间单元
	CAN_InitStructure.CAN_BS2=CAN_BS2_3tq;		   //BTR-TS1 时间段2 占用了3个时间单元	

	/* CAN Baudrate = 1 MBps (1MBps已为stm32的CAN最高速率) (CAN 时钟频率为 APB1 = 36 MHz) */
	CAN_InitStructure.CAN_Prescaler =4;		   		//BTR-BRP 波特率分频器  定义了时间单元的时间长度 36/(1+5+3)/4=1 Mbps
	CAN_Init(CAN1, &CAN_InitStructure);
	


	//滤波器参数配置******************************************************************
	/*CAN筛选器初始化*/
	CAN_FilterInitStructure.CAN_FilterNumber=0;						//筛选器组0
	CAN_FilterInitStructure.CAN_FilterMode=CAN_FilterMode_IdMask;	//工作在掩码模式
	CAN_FilterInitStructure.CAN_FilterScale=CAN_FilterScale_32bit;	//筛选器位宽为单个32位。
	/* 使能筛选器,按照标志的内容进行比对筛选,扩展ID不是如下的就抛弃掉,是的话,会存入FIFO0。 */

	CAN_FilterInitStructure.CAN_FilterIdHigh= (((u32)other_id<<3)|CAN_ID_EXT|CAN_RTR_DATA)>>16;	//要筛选的ID高位 
	CAN_FilterInitStructure.CAN_FilterIdLow=  (((u32)other_id<<3)|CAN_ID_EXT|CAN_RTR_DATA)&0xffff; //要筛选的ID低位 
	CAN_FilterInitStructure.CAN_FilterMaskIdHigh= 0xffff;			//筛选器高16位每位必须匹配
	CAN_FilterInitStructure.CAN_FilterMaskIdLow= 0xffff;			//筛选器低16位每位必须匹配
	CAN_FilterInitStructure.CAN_FilterFIFOAssignment=CAN_Filter_FIFO0 ;				//筛选器被关联到FIFO0
	CAN_FilterInitStructure.CAN_FilterActivation=ENABLE;			//使能筛选器
	CAN_FilterInit(&CAN_FilterInitStructure);
	/*CAN通信中断使能*/
	CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE);
	
}

/**
  * @brief  初始化 Rx Message数据结构体
  * @param  RxMessage: 指向要初始化的数据结构体
  * @retval None
  */
void Init_RxMes(CanRxMsg *RxMessage)
{
	uint8_t ubCounter = 0;

	/*把接收结构体清零*/
	RxMessage->StdId = 0x00;
	RxMessage->ExtId = 0x00;
	RxMessage->IDE = CAN_ID_STD;
	RxMessage->DLC = 0;
	RxMessage->FMI = 0;
	for (ubCounter = 0; ubCounter < 8; ubCounter++)
	{
		RxMessage->Data[ubCounter] = 0x00;
	}
}


/*
 * 函数名:CAN_SetMsg
 * 描述  :CAN通信报文内容设置,设置一个数据内容为0-7的数据包
 * 输入  :发送报文结构体
 * 输出  : 无
 * 调用  :外部调用
 */	 
void CAN_SetMsg(CanTxMsg *TxMessage)
{	  

//	TxMessage->StdId=my_id;						 //使用标准ID 11bit	 
	TxMessage->ExtId=my_id;						 //使用的扩展ID 29bit
	TxMessage->IDE=CAN_ID_EXT;					 //扩展模式
	TxMessage->RTR=CAN_RTR_DATA;				 //发送的是数据
	TxMessage->DLC=8;							 //数据长度为8字节

	TxMessage->Data[0]=my_dat0;
	TxMessage->Data[1]=my_dat1;
	TxMessage->Data[2]=my_dat2;
	TxMessage->Data[3]=my_dat3;
	TxMessage->Data[4]=my_dat4;
	TxMessage->Data[5]=my_dat5;
	TxMessage->Data[6]=my_dat6;
	TxMessage->Data[7]=my_dat7;
}



注解

其中有几个点容易搞混淆或难以理解,程序中部分注解如下:

/*
 * 函数名:CAN_SetMsg
 * 描述  :CAN通信报文内容设置,设置一个数据内容为0-7的数据包
 * 输入  :发送报文结构体
 * 输出  : 无
 * 调用  :外部调用
 */	 
void CAN_SetMsg(CanTxMsg *TxMessage)
{	  

//	TxMessage->StdId=my_id;						 //使用标准ID 11bit	 
	TxMessage->ExtId=my_id;						 //使用的扩展ID 29bit
	TxMessage->IDE=CAN_ID_EXT;					 //扩展模式
	TxMessage->RTR=CAN_RTR_DATA;				 //发送的是数据
	TxMessage->DLC=8;							 //数据长度为8字节

	TxMessage->Data[0]=my_dat0;
	TxMessage->Data[1]=my_dat1;
	TxMessage->Data[2]=my_dat2;
	TxMessage->Data[3]=my_dat3;
	TxMessage->Data[4]=my_dat4;
	TxMessage->Data[5]=my_dat5;
	TxMessage->Data[6]=my_dat6;
	TxMessage->Data[7]=my_dat7;
}

源程序中,这段函数用于设置发送的报文信息

  • TxMessage->IDE=CAN_ID_STD; //标准模式,是用于设置报文的id为标准id,标准id中,将使用StdId变量作为id,他的有效位数为11bit,0x00000000至0x000007ff有效,仅低11bit有效
  • TxMessage->IDE=CAN_ID_EXT; //扩展模式,是用于设置报文的id为扩展id,扩展id中,将使用ExtId变量作为id,他的有效位数为29bit,0x00000000至0x1fffffff有效,高位3bit为无效位置。
  • id越小优先级越高。
  • TxMessage->RTR=CAN_RTR_DATA; //发送的是数据,表示发送的报文为数据类型
  • TxMessage->DLC=8; //数据长度为8字节,表示数据长度8个字节
  • 后续则是具体数据内容。

在实际调试中,波特率一般多个can模块配置相同即可,具体速度影响不大,如果不想改可以之间使用上面的CAN1_Config配置。

如果测试时,接收不到信息,往往是滤波器没有配置好,上述程序关于滤波器配置详细解释一下:

//滤波器参数配置******************************************************************
	/*CAN筛选器初始化*/
	CAN_FilterInitStructure.CAN_FilterNumber=0;						//筛选器组0
	CAN_FilterInitStructure.CAN_FilterMode=CAN_FilterMode_IdMask;	//工作在掩码模式
	CAN_FilterInitStructure.CAN_FilterScale=CAN_FilterScale_32bit;	//筛选器位宽为单个32位。
	/* 使能筛选器,按照标志的内容进行比对筛选,扩展ID不是如下的就抛弃掉,是的话,会存入FIFO0。 */

	CAN_FilterInitStructure.CAN_FilterIdHigh= (((u32)other_id<<3)|CAN_ID_EXT|CAN_RTR_DATA)>>16;	//要筛选的ID高位 
	CAN_FilterInitStructure.CAN_FilterIdLow=  (((u32)other_id<<3)|CAN_ID_EXT|CAN_RTR_DATA)&0xffff; //要筛选的ID低位 
	CAN_FilterInitStructure.CAN_FilterMaskIdHigh= 0xffff;			//筛选器高16位每位必须匹配
	CAN_FilterInitStructure.CAN_FilterMaskIdLow= 0xffff;			//筛选器低16位每位必须匹配
	CAN_FilterInitStructure.CAN_FilterFIFOAssignment=CAN_Filter_FIFO0 ;				//筛选器被关联到FIFO0
	CAN_FilterInitStructure.CAN_FilterActivation=ENABLE;			//使能筛选器
	CAN_FilterInit(&CAN_FilterInitStructure);
  • CAN_FilterInitStructure.CAN_FilterNumber=0;,表示选择滤波器组0,stm32f103系列can过滤器有14组。可以配置多组滤波器。
  • CAN_FilterInitStructure.CAN_FilterMode如下:
    • CAN_FilterInitStructure.CAN_FilterMode=CAN_FilterMode_IdMask;,表示滤波器工作在掩码模式,也就是只要特定的几位id符合,就接受数据。例如滤波器值为0x12345678;掩码为0x0000ffff,则接收的id只需要满足0Xxxxx5678,就可以被接收。
    • CAN_FilterInitStructure.CAN_FilterMode=CAN_FilterMode_IdList;,表示滤波器工作在列表模式,此时只有接收数据id等于滤波器值,或者接收数据id等于掩码值,才被接收。
  • CAN_FilterInitStructure.CAN_FilterScale可以选择两种CAN_FilterScale_16bit,CAN_FilterScale_32bit。
模式CAN FilterldHighCAN FilterldLowCAN FilterMaskIdHighCAN FilterMaskIdLow
32位列表模式ID1 的高16位ID1 的低16位ID2 的高16位ID2 的低16位
16位列表模式ID1 的完整数值ID2 的完整数值ID3 的完整数值ID4 的完整数值
32位掩码模式ID1 的高16位ID1 的低16位ID1 掩码的高16位ID1 掩码的低16位
16位掩码模式ID1 的完整数值ID2 的完整数值ID1 掩码的完整数值ID2 掩码完整数值
  • CAN_FilterInitStructure.CAN_FilterIdHighCAN_FilterInitStructure.CAN_FilterIdLow,表示滤波器值的高16bit和低16bit,CAN_FilterInitStructure.CAN_FilterMaskIdHighCAN_FilterInitStructure.CAN_FilterMaskIdHigh表示掩码值的高16bit和低16bit。使用扩展id时,他们和id的关系如下:
bit31bit30bit29bit28bit27bit26bit25bit24
EXID28EXID27EXID26EXID25EXID24EXID23EXID22EXID21
bit23bit22bit21bit20bit19bit18bit17bit16
EXID20EXID19EXID18EXID17EXID16EXID15EXID14EXID13
bit15bit14bit13bit12bit11bit10bit9bit8
EXID12EXID11EXID10EXID9EXID8EXID7EXID6EXID5
bit7bit6bit5bit4bit3bit2bit1bit0
EXID4EXID3EXID2EXID1EXID0IDERTR0

具体可以查看芯片手册:
在这里插入图片描述

  • CAN_FilterInitStructure.CAN_FilterFIFOAssignment=CAN_Filter_FIFO0 ,则表示接收数据暂存fifo0,可选参数还有CAN_Filter_FIFO1;
  • CAN_FilterInitStructure.CAN_FilterActivation=ENABLE;,则表示使能滤波器。

实验现象

没有修改程序的情况下,分别烧录三块核心板,连接到制作的can总线上,现象为:

  • 核心板PC13引脚是灯,1s一闪,说明程序跑起来了。
  • A板子id为0x00000001,B板子id为0x00000002,C板子id为0x00000003,A板子只接受B板子信号,B板子只接收C板子信号,C板子只接收A板子信号。
  • 上电后,所有板子每1s都会发送一次消息
  • PA9是串口发送引脚,收到can数据都,会通过串口1的PA9引脚发送出去
  • 在电脑上使用串口助手就可以看到数据了。
  • 如果说你没有串口。。。。。。debug看看运行时能不能进接收中断。
  • 注意总线要接上拉电阻到3.3v,两个单片机之间gnd需要用线连接起来共地。rx直接接总线 tx加二极管接到总线上。

连接A板子串口,可以看到以下信息
在这里插入图片描述
B板子收到消息如下
在这里插入图片描述
还有c板子懒得接了,一样的道理。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值