STM32 bootload设计及思路,从理解到实施

连载教程,保姆级教学

设计思路

闲来无事,博文好酒也没更新了。
思来想去,还是觉得写个小文章。
stm32也接触那么多年了,期间也做了很多的小玩意,不过大都是一些以学习为目的的DIY。
博主本身的话还是比较喜欢一些精细小巧的东西,在画电路板的时候,体积往往是尽可能的小一些,导致很多时候电路板的空间并不富裕,甚至于连stm32的烧录接口都没有留,于是就有了IAP的需求。

设计步骤

一般的IAP,分为三个部分。
1 上位机:负责发送新的固件給单片机
2 bootload: 负责固件的版本检测和更新,app程序的跳转和备份
3 app:负责程序的运行

bootload的设计

bootload的设计,一般都是通过通讯接口来接受bin文件,包括但不限于uart,spi,iic。本期教程采用uart来做讲解
教程开始前,请完成一下功能的验证
1 串口数据的收发
2 flash的读写和擦除
3 默认以上两点都已学会并熟练掌握
4 没有了
开个玩笑,现在开始正式讲解
bootload在上文中已经提及过具体的内容,分别是固件的检测和更新,在程序运行的时候检测固件是否需要更新,方法有很多种,在这里笔者推荐2种方式
1 程序初始化后,在一定的时间内,循环向上位机发送数据,可将当前固件版本号发送给上位机,然后上位机进行版本对比,若需要升级,则下发命令,单片机进入接收数据的状态
2 板载按钮,程序初始化后,在一定的时间内,若检测到按钮被按下,则单片机进入接收数据的状态
以上方法皆可,包括也不限于这两种方法,笔者这里自己设计的板子,带有触摸显示屏,单片机开始的时候刷屏,提示是否进入固件接收状态,点击屏幕即可进入接收状态。
以下是程序逻辑

	HAL_Init();				//hal init
	SystemClock_Config();	//system init
  	MX_GPIO_Init();			//gpio init	初始化按键
  	MX_DMA_Init();			//dma init	spi dma 初始化	刷屏使用
  	MX_SPI1_Init();			//spi init	spi 初始化	刷屏使用
  	MX_USART1_UART_Init();	//uart init	uart 初始化	用于和上位机通讯
 	MX_ADC2_Init();			//adc init	电池电量检测
  	MX_TIM2_Init();			//tim init	定时器
	HAL_UART_Receive_IT(&huart1,&UART1struct.RX_Tmp,1);	//开启串口接收中断
	if(HAL_GPIO_ReadPin(PWR_BTN_GPIO_Port,PWR_BTN_Pin)== GPIO_PIN_SET)
	{
		//开机检测,判断是插电开机还是按键开机,并锁存供电按钮
		HAL_GPIO_WritePin(PWR_CTRL_GPIO_Port,PWR_CTRL_Pin,GPIO_PIN_SET);
		while(check_pwr_btn());
	}
	//lcd init
	LCD_Init();		
	LCD_Fill(0,0,LCD_W,LCD_H,BLACK);
	LCD_BLK_Set();
	CST816T_Init();
	
	UI_Init();	//重点,在此处进行判断,刷屏询问是否要升级固件,如果不需要升级,则进行app加载(判断bootflag的状态)
	if(bootflag == 0)	//不需要升级
	{
		JumpToApp();	//则进行程序跳转
	}
	UI_Upgrade();		//刷屏,显示程序进行升级中
  	while (1)
  	{
		//在开始传输数据之前,每500ms向上位机发送一次数据,获取固件的信息
		if(check_fw_flag == 0)
		{
			//ymodem协议,循环向上位机发送C,告诉上位机,自己已进入数据接收状态
			HAL_UART_Transmit(&huart1,&C,1,100);
			HAL_Delay(1000);
		}
		//关机
		if(HAL_GPIO_ReadPin(PWR_BTN_GPIO_Port,PWR_BTN_Pin)== GPIO_PIN_SET)
		{
			while(check_pwr_btn());
			HAL_GPIO_WritePin(PWR_CTRL_GPIO_Port,PWR_CTRL_Pin,GPIO_PIN_RESET);
			__set_FAULTMASK(1);
			HAL_NVIC_SystemReset();
			HAL_Delay(5000);
		}
  }

主程序大概的逻辑就是这样子了

bootload数据的接收处理函数

在学习接收数据的方法之前,要了解一个协议,Ymodem协议,网上有很多详细的讲解,笔者在这里简单的介绍一下,着重讲解一下实现

//符号       数值      含义
//SOH       0X01        128字节数据包
//STX       0X02        1024字节数据包
//EOT       0X04        结束传输
//ACK       0X06        回应
//NAK       0X15        不回应
//CA        0X18        传输终止
//C         0X43        请求数据包

在主函数的代码片里面可以看到

	//ymodem协议,循环向上位机发送C,告诉上位机,自己已进入数据接收状态
	HAL_UART_Transmit(&huart1,&C,1,100);
	HAL_Delay(1000);

这里就是在上位机发送数据之前,每隔1ms向上位机发送一个0x43 的数据,告诉上位机自己进入等待数据接收的状态

然后我们看接收数据的实现,数据接收分为两部分,第一部分是不定长数据的接收,第二部分是不定长数据接收的判断

串口不定长接收处理

这里采用的是hal库,因此在串口接收到数据后,会进入串口接收回调函数里,在这里重写数据接收逻辑。笔者这里初始化了一个定时器,当串口在定时器计时时间内收到数据,定时器被重置,继续计时,当计时超时,则进行数据处理

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    UNUSED(huart);
	UART1struct.RX_TempBuff[UART1struct.RX_TempLen] = UART1struct.RX_Tmp;	//将接收到的数据放到缓存数组中
	UART1struct.RX_TempLen++;							//缓存下标+1
	HAL_UART_Receive_IT(&huart1,&UART1struct.RX_Tmp,1);	//开启串口接收中断
	HAL_TIM_Base_Start_IT(&htim2);						//开启定时器
	__HAL_TIM_SET_COUNTER(&htim2,0);					//定时器计时清零
}

定时器回调函数

//符号       数值      含义
//SOH       0X01        128字节数据包
//STX       0X02        1024字节数据包
//EOT       0X04        结束传输
//ACK       0X06        回应
//NAK       0X15        不回应
//CA        0X18        传输终止
//C         0X43        请求数据包
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{   
	char file_name[64] = {0};						//接收到的文件名
	int file_len = 0;								//接收到的文件长度
	int i = 0,j = 0;								//临时变量
	uint8_t number = 0;								//临时变量
	uint32_t write_addr = 0;						//写入的地址
	if(htim == &htim2)  //判断中断是否来自于定时器2
	{
		HAL_TIM_Base_Stop_IT(&htim2);				//关闭定时器
		HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);	//反转指示灯状态
		switch(UART1struct.RX_TempBuff[0])			//判断帧头数据
		{
			case 0x01:	//握手完成,上位机发送启示帧,协议开始传输,协议格式
			{			//SOH  | 0X00 0XFF | FileName+0x00  | FileSize+0x00 | NULL(0X00)
						//0x01 | 0x00 0xff | watch.bin 0x00 | 123456 0x00   | 0x00...
				check_fw_flag = 1;			//将向上位机发送请求数据的标志位置1,之后单片机将被不会继续上上位机发送数据请求标识符
				for(i = 0; i <UART1struct.RX_TempLen-3;i++)			//拷贝数组
				{
					UART1struct.RX_Buff[i] = UART1struct.RX_TempBuff[i+3];
				}
				sscanf((const char*)UART1struct.RX_Buff,"%s",file_name);	//解析文件名
				j = strlen(file_name);										
				for(i = 0; i <UART1struct.RX_TempLen-3;i++)
				{
					UART1struct.RX_Buff[i] = UART1struct.RX_Buff[i+j+1];
				}
				sscanf((const char*)UART1struct.RX_Buff,"%d",&file_len);	//解析文件大小
				i = EraseFlash(APP_STAR_ADDR,APP_STOP_ADDR);				//擦除flash
				if(i == 2)													//判断擦除是否成功
				{
					EraseFlash(APP_STAR_ADDR,APP_STOP_ADDR);
				}
				HAL_UART_Transmit(&huart1,&ACK,1,100);						//向上位机发送应答信号
			}break;
			case 0x02:	//数据接收函数处理
			{
				if(UART1struct.RX_TempBuff[1]+UART1struct.RX_TempBuff[2] == 0xff)	//校验帧头数据的合法性
				{
					write_addr = UART1struct.RX_TempBuff[1]*1024;	//flash写入数据的地址,等于帧头(包号*1024)
					for(i = 0; i <UART1struct.RX_TempLen-3;i++)		//将数据拷贝出来
					{
						UART1struct.RX_Buff[i] = UART1struct.RX_TempBuff[i+3];
					}
					UART1struct.RX_Len = UART1struct.RX_TempLen-3;
					FlashWriteAPP(APP_STAR_ADDR+write_addr,UART1struct.RX_Buff,UART1struct.RX_Len);		//将数据写入flash
					HAL_UART_Transmit(&huart1,&ACK,1,100);			//向上位机发送应答信号
				}
			}break;
			case 0x04:	//结束传输,程序重启
			{
				if(UART1struct.RX_TempBuff[1]+UART1struct.RX_TempBuff[2] == 0xff)
				{
					HAL_GPIO_WritePin(PWR_CTRL_GPIO_Port,PWR_CTRL_Pin,GPIO_PIN_RESET);
					__set_FAULTMASK(1);
					HAL_NVIC_SystemReset();
					HAL_Delay(5000);
//					JumpToApp();
				}
			}
			default:
				break;
		}
		
//		FlashWriteAPP(APP_STAR_ADDR,UART1struct.RX_TempBuff,UART1struct.RX_TempLen);
		for(i = 0; i < 1033; i++)		//数组里面的数据处理后,将数据全部清0,以免数据出现意外
		{
			UART1struct.RX_TempBuff[i] = 0;
			UART1struct.RX_Buff[i] = 0;
		}
		UART1struct.RX_TempLen= 0;
		UART1struct.RX_Len = 0;
	}
}

程序的跳转

将程序跳转也是分为两部分
第一部分是将程序跳转至app的地址,实现如下

__IO uint32_t BootAddr = 0x08010000;

void (*SysMemBootJump)(void);

static void JumpToApp(void)
{
	uint32_t i = 0;
	//关闭全局中断
	DISABLE_INT();
	//关闭滴答定时器,复位到默认值
	SysTick->CTRL = 0;
	SysTick->LOAD = 0;
	SysTick->VAL  = 0;
	//关闭所有中断,清除中断挂起标志
	for(i = 0; i < 8; i++)
	{
		NVIC->ICER[i] = 0xffffffff;
		NVIC->ICPR[i] = 0xffffffff;
	}
	//使能全局中断
	ENABLE_INT();
	//跳转到系统bootload,首地址时MSP, 地址+4是复位中断服务程序地址
	SysMemBootJump = (void (*)(void)) (*((uint32_t *) (BootAddr + 4)));
	/* 设置主堆栈指针 */
	__set_MSP(*(uint32_t *)BootAddr);
	/* 在RTOS工程,这条语句很重要,设置为特权级模式,使用MSP指针 */
	__set_CONTROL(0);
	/* 跳转到系统BootLoader */
	SysMemBootJump(); 
	/* 跳转成功的话,不会执行到这里,用户可以在这里添加代码 */
	while (1)
	{
		HAL_GPIO_TogglePin(LED_GPIO_Port,LED_Pin);
		HAL_Delay(100);
	}
}

第二部分是在app中实现的,因为app的起始地址和bootload的起始地址不一样,跳转完成后,中断向量映射的地址也不同,因此需要在app中重新设置中断向量映射地址,这里简单说一下
修改system_stm32xxx.h文件中的VECT_TAB_OFFSET地址,这里就是设置偏移地址的值,在函数SystemInit这里可看出具体的实现,这里就不多说了

void SystemInit(void)
{
	/* FPU settings ------------------------------------------------------------*/
	#if (__FPU_PRESENT == 1) && (__FPU_USED == 1)
	SCB->CPACR |= ((3UL << (10*2))|(3UL << (11*2)));  /* set CP10 and CP11 Full Access */
	  #endif
	
	  /* Configure the Vector Table location add offset address ------------------*/
	#if defined(USER_VECT_TAB_ADDRESS)
	  SCB->VTOR = VECT_TAB_BASE_ADDRESS |  VECT_TAB_OFFSET

; /* Vector Table Relocation in Internal SRAM */
	#endif /* USER_VECT_TAB_ADDRESS */
}

上位机的实现

上位机还是老一套,实现的功能为当打开程序时,自动检测串口,当识别为ch340的设备后打开该设备的com口,然后打开bin文件和下发文件,在这里只贴出效果和简单的串口命令下发的代码。如果需要源码,则可以自行下载
下图分别为未插入ch340设备的界面,插入ch340后打开的界面,以及选择bin文件后的界面
在这里插入图片描述

/// <summary>
        /// 打开可用的串口
        /// </summary>
        private void OpenUseSerialPort()
        {
            if (ConnectState.BackColor != Color.Lime)
            {
                //"0403", "6001" USB转232
                //"067B", "2303" USB转485
                //"0986", "0320" PLC端口
                //"1a86", "7523" CH340
                //portName = FormMain.GetPortNameFormVidPid("0986", "0320");
                List<string> availport;
                availport = GetPortNameFormVidPid("1a86", "7523");
                //portName = GetPortNameFormVidPid("067B", "2303");
                if (availport != null)
                {
                    string[] port = availport.ToArray();
                    CBVarPort.Items.AddRange(port);

                    //timer1.Enabled = true;
                }
                else
                {
                    CBVarPort.Items.AddRange(System.IO.Ports.SerialPort.GetPortNames());  //获取所有串口设备
                    DisplayLog(1, "NO find Correspond port!");
                    return;
                }
                if (CBVarPort.Items.Count > 0)
                {
                    CBVarPort.SelectedIndex = 0;

                    if (mc.IsOpen == false)
                    {
                        try
                        {
                            portName = CBVarPort.Items[0].ToString();
                            Debug.WriteLine("Use port:" + portName);

                            // bool isOpen = mc.Open(portName, Baudrate, System.IO.Ports.Parity.None, 8, System.IO.Ports.StopBits.One, 200, 200);
                            mc.PortName = portName;
                            mc.BaudRate = Baudrate;
                            mc.Parity = Parity.None;
                            mc.StopBits = StopBits.One;
                            mc.DataBits = 8;
                            mc.Open();

                            if (!mc.IsOpen)
                            {
                                //MessageBox.Show("串口打开失败");
                                Console.ForegroundColor = ConsoleColor.Red;
                                DisplayLog(1, "* Open port fail");
                                Console.ForegroundColor = ConsoleColor.Green;

                            }
                            else
                            {
                                //串口打开成功,modbus协议初始化成功,打开定时器
                                //MessageBox.Show("串口打开成功");
                                ConnectState.Text = "Connect";
                                ConnectState.BackColor = Color.Lime;
                                DisplayLog(1, "* Open port ok");
                                mc.Write("data\r\n");
                            }

                        }
                        catch (Exception ex)
                        {
                            MessageBox.Show("Open port error" + ex, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
                            ConnectState.Text = "NULL";
                            ConnectState.BackColor = Color.Yellow;
                            Console.ForegroundColor = ConsoleColor.Red;
                            DisplayLog(1, "* Open port error");
                            Console.ForegroundColor = ConsoleColor.Green;
                        }
                    }
                }
                else
                {
                    MessageBox.Show("NO Use Serialport", "Waring", MessageBoxButtons.OK, MessageBoxIcon.Question);
                    Console.ForegroundColor = ConsoleColor.Red;
                    DisplayLog(1, "* No available port");
                    Console.ForegroundColor = ConsoleColor.Green;
                }
            }
            else
            {
                mc.Close();
                ConnectState.Text = "NULL";
                ConnectState.BackColor = Color.Yellow;
                DisplayLog(1, "* Port close");
            }

            //mc.ReadTimeout = 50;
            mc.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler);
        }
      
//Ymodem传输协议
        //符号       数值      含义
        //SOH       0X01        128字节数据包
        //STX       0X02        1024字节数据包
        //EOT       0X04        结束传输
        //ACK       0X06        回应
        //NAK       0X15        不回应
        //CA        0X18        传输终止
        //C         0X43        请求数据包
        byte SOH = 0x01;
        byte STX = 0X02;
        byte EOT = 0X04;
        byte ACK = 0X06;
        byte NAK = 0X15;
        byte CA = 0X18;
        byte C = 0X43;
        static byte[] sendData= new byte[1033];
        private void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e)
        {
            SerialPort sp = (SerialPort)sender;
            Thread.Sleep(100);
            try
            {
                string indata = sp.ReadExisting();
                try
                {
                    if (sendFlag == true)
                    {
                        if (indata[0] == C)     //请求数据传输
                        {
                            DisplayLog(1, "handshake");
                            int len1 = 0;
                            int len2 = 0;
                            sendData[0] = SOH;      //帧头
                            sendData[1] = 0x00;     //包号
                            sendData[2] = 0xff;     //包号反码
                            byte[] decBytes = System.Text.Encoding.UTF8.GetBytes(label1.Text);
                            decBytes.CopyTo(sendData, 3);//将文件名追加到sendData中
                            len1 = label1.Text.Length;
                            sendData[3 + len1] = 0x00;
                            byte[] decBytes1 = System.Text.Encoding.UTF8.GetBytes(label4.Text);
                            decBytes1.CopyTo(sendData, 4 + len1);
                            len2 = label4.Text.Length;
                            for (int i = (4 + len1 + len2); i < 131; i++)
                            {
                                sendData[i] = 0x00;
                            }
                            mc.Write(sendData, 0, 131);
                        }
                        if (indata[0] == ACK)
                        {
                            if (number < point)
                            {
                                sendData = lists[number].ToArray();
                                mc.Write(sendData, 0, 1027);
                                number++;
                                progressBar1.PerformStep();
                                int j = (int)(((float)number / (float)point)*100);
                                label5.Text = j.ToString() + "%";
                                DisplayLog(1, point.ToString()+" -> "+number);
                            }
                            else
                            {
                                sendData[0] = EOT;
                                sendData[1] = 0x00;
                                sendData[2] = 0xff;
                                for (int i = 3; i < 133; i++)
                                {
                                    sendData[i] = 0x00;
                                }
                                mc.Write(sendData, 0, 131);
                                DisplayLog(1, "Send Over!");
                                point = 0;
                                number = 0;
                                sendFlag = false;
                            }
                        }
                    }
                }
                catch (Exception ex)
                {

                }
            }
            catch (Exception ex)
            {

            }

        }
/// <summary>
        /// 通过vid,pid获得串口设备号
        /// </summary>
        /// <param name="vid">vid</param>
        /// <param name="pid">pid</param>
        /// <returns>串口号</returns>
        public static List<string> GetPortNameFormVidPid(string vid, string pid)
        {
            Guid myGUID = Guid.Empty;

            string enumerator = "USB";
            //string enumerator = "CH340";
            List<string> AvailPort = new List<string>(16);
            try
            {
                IntPtr hDevInfo = HardWareLib.SetupDiGetClassDevs(ref myGUID, enumerator, IntPtr.Zero, HardWareLib.DIGCF_ALLCLASSES | HardWareLib.DIGCF_PRESENT);
                if (hDevInfo.ToInt32() == HardWareLib.INVALID_HANDLE_VALUE)
                {
                    throw new Exception("没有该类设备");
                }
                HardWareLib.SP_DEVINFO_DATA deviceInfoData;//想避免在api中使用ref,就把structure映射成类
                deviceInfoData = new HardWareLib.SP_DEVINFO_DATA();
                deviceInfoData.cbSize = 28;//如果要使用SP_DEVINFO_DATA,一定要给该项赋值28=16+4+4+4
                deviceInfoData.devInst = 0;
                deviceInfoData.classGuid = System.Guid.Empty;
                deviceInfoData.reserved = 0;
                UInt32 i;
                StringBuilder property = new StringBuilder(HardWareLib.MAX_DEV_LEN);

                for (i = 0; HardWareLib.SetupDiEnumDeviceInfo(hDevInfo, i, deviceInfoData); i++)
                {
                    //       Console.Write(deviceInfoData.classGuid.ToString());
                    //       HardWareOperation.SetupDiGetDeviceInstanceId(hDevInfo, deviceInfoData, porperty, (uint)porperty.Capacity, 0);
                    HardWareLib.SetupDiGetDeviceRegistryProperty(hDevInfo, deviceInfoData, (uint)HardWareLib.SPDRP_.SPDRP_CLASS, 0, property, (uint)property.Capacity, IntPtr.Zero);

                    if (property.ToString().ToLower() != "ports") continue;//首先看看是不是串口设备(有些USB设备不是串口设备)

                    HardWareLib.SetupDiGetDeviceRegistryProperty(hDevInfo, deviceInfoData, (uint)HardWareLib.SPDRP_.SPDRP_HARDWAREID, 0, property, (uint)property.Capacity, IntPtr.Zero);

                    if (!(property.ToString().ToLower().Contains(vid.ToLower()) && property.ToString().ToLower().Contains(pid.ToLower()))) continue;//找到对应于vid&pid的设备

                    HardWareLib.SetupDiGetDeviceRegistryProperty(hDevInfo, deviceInfoData, (uint)HardWareLib.SPDRP_.SPDRP_FRIENDLYNAME, 0, property, (uint)property.Capacity, IntPtr.Zero);
                    //                     break;     //查询到一个就退出
                    string friendlyName = property.ToString();
                    char[] separatorMark = { '(', ')' };
                    string[] strList1 = friendlyName.Split(separatorMark);
                    if (strList1[1].Substring(0, 3) == "COM")
                    {
                        AvailPort.Add(strList1[1]);
                    }
                }

                HardWareLib.SetupDiDestroyDeviceInfoList(hDevInfo);//记得用完释放相关内存

                if (AvailPort.Count > 0)
                {
                    return AvailPort;
                }
                else

                    return null;

            }
            catch (Exception ex)
            {
                //        MessageBox.Show(ex.Message);
                return null;
            }
        }
/// <summary>
        /// 打开BIN文件得到路径,读取其中的二进制内容
        /// </summary>
        /// <returns>返回二进制数字符串</returns>
        /// 
        public static int OpenBinFile()
        {
            //string bin_str = "";
            //byte[] binchar = new byte[] { };
            int file_len;
            //打开文件类
            
            OpenFileDialog dialog = new OpenFileDialog();
            //使用当前目录作为初始目录
            dialog.InitialDirectory = System.AppDomain.CurrentDomain.BaseDirectory;
            //文件过滤,仅打开bin
            dialog.Filter = "bin文件(*.bin)|*.bin";
            //关闭选择多文件
            dialog.Multiselect = false;
            if (dialog.ShowDialog() == DialogResult.OK)
            {
                //文件流类//用于文件的读写与关闭//来自于System.IO 
                FileStream fileStream = new FileStream(dialog.FileName, FileMode.Open);
                //读二进制文件类
                BinaryReader br = new BinaryReader(fileStream, Encoding.UTF8);
                //获取bin文件长度
                file_len = (int)fileStream.Length;
                //得到所有字节
                //binchar = br.ReadBytes(file_len);
                dddd = br.ReadBytes(file_len);
                file_name = fileStream.Name;
                file_name = Path.GetFileName(file_name);
                fileStream.Close();
                return file_len;
            }

            //返回字符串
            
            return 0;
        }
/// <summary>
        /// start/stop按钮单击事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        //private List<byte> list = new List<byte>();
        private List<byte> list = null;
        private List<List<byte>> lists = new List<List<byte>>();
        private void button2_Click(object sender, EventArgs e)
        {
            int len = OpenBinFile();
            int leng = len % 1024;
            open_file_len = len;
            label4.Text = len.ToString();
            label1.Text = file_name;

            var index = 0;
            //list = null;
            foreach (var item in dddd)
            {
                if(list == null)
                {
                    list = new List<byte>();
                }
                if (list.Count == 0)
                {
                    list.Add(STX);
                    list.Add(((byte)point));
                    list.Add((byte)~point);
                }

                list.Add(item);
                if (++index == 1024)
                {
                    lists.Add(list);
                    list =null;
                    index = 0;
                    point++;
                }
            }
            for (int i = 0; i < (1024-leng); i++)
            {
                list.Add(0x1A);
            }
            lists.Add(list);
            point++;
            progressBar1.Visible = true;
            progressBar1.Style = ProgressBarStyle.Blocks;
            progressBar1.Maximum = (int)point;
            progressBar1.Minimum = 0;
            progressBar1.MarqueeAnimationSpeed = 0;
            progressBar1.Step = 1;
            label5.Text = "0%";
            label5.Visible = true;
        }

各位看官记得收藏关注,后续将上位机源码上传

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值