STM32教程第三讲:串口调试技巧

文章目录

  • 需求
  • 一、串口调试的意义
  • 二、串口调试实现流程
  • 1.开时钟
  • 2.配置对应的IO口
  • 3.配置串口1
  • 4.printf函数的重定向
  • 三、需求的实现
  • 总结

  • 需求

    1.设备上电后四个灯灭。
    2.按下KEY1,LED1灯亮,同时串口发送“LED1灯亮”。
    3.再次按下KEY1,LED1灯灭,同时串口发送“LED1灯灭”。
    4.以此类推,设置KEY2,KEY3,KEY4以及对应的LED灯。


    一、串口调试的意义

    在linux和C语言的学习中,每当编译完程序时代码编译没错,但功能实现却出现错误时,我们通常会使用printf来打印一下,观察程序到底是运行到哪里出错了。
    在STM32中,由于现阶段还没有学习屏幕显示,当你在程序中直接添加printf时,我们无法观察到具体现象。此时,学会如何通过串口来调试就显得及其重要了。

    二、串口调试实现流程

    1.开时钟

    首先,打开原理图找到USART1(串口)的位置

    打开数据手册,找到系统结构图,查看控制串口和引脚的时钟在那个总线。

    由上图中可知,控制USART1和GPIOA的时钟均在APB2时钟总线上。

    查看手册中APB2外设时钟使能寄存器是如何控制的:

    由上图可知,控制USART1和GPIOA的时钟是由APB2外设时钟使能寄存器的第14位和第2位控制的。该模块均为1使能0关闭。

    在代码中为:

    	//开时钟:GPIOA,USART1
    	  RCC->APB2ENR |= 0x01<<2;//GPIOA
    	  RCC->APB2ENR |= 0x01<<14;//USART1
    

    2.配置对应的IO口

    由于配置对应的IO口 PA9(TX)和 PA10(RX)都是高八位,所以直接打开手册找到端口配置高寄存器。

    模式的确定是由手册中8.1.11 外设的GPIO配置确定的

    通信方式:
    串行:串行数据传输时,数据是一位一位地在通信线上传输的。
    并行:并行通信传输中有多个数据位,同时在两个设备之间传输。
    单工:设备只能发送,或者只能接收 只有一种功能,比如收音机。
    双工:设备能够发送,也能够接收。
    全双工:发送和接收可以独立工作,可以同时进行收发,互不影响。
    半双工:发送的时候不能接收,接收的时候不能发送 比如对讲机。
    同步:设备之间使用同一个时钟线。
    异步:设备有自己的时钟,但是通信时时钟要设置成一样。
    时钟作用:决定了通信的速率。
    通常情况下我们使用的串口都是串行全双工异步通信接口。
    所以此时我们要将PA9(TX)配置为复用推挽(1011),PA10(RX)配置成浮空输入(0100)
    代码如下:

    	//配置对应的IO口	
    		GPIOA->CRH &= ~(0x0f<<4);//PA9清0
    		GPIOA->CRH |= 0x0B<<4;//设置成复用推挽输出
    		GPIOA->CRH &= ~(0x0f<<8);//PA10清0
    		GPIOA->CRH |= 0x04<<8;//设置成浮空
    

    3.配置串口1

    串口1配置时需要设置数据位、校验位、停止位和波特率。
    本次配置为8数据位,0校验位,1停止位,115200波特率
    在手册中找到串口的控制寄存器CR1和CR2


    在CR1中可以看到数据位在12位,校验位在第10位,发送使能在第3位,串口使能在第13位。
    要注意的是发送使能和串口使能要最后再配制,防止没配置好串口就开始工作了。
    在CR2中可以看到停止位在13和12位,此时全配1即可。
    代码如下:

            USART1->CR1 &= ~(0x01<<12);//数据位置0
    		USART1->CR1 &= ~(0x01<<10);//设置0位校验位
    		USART1->CR2 &= ~(0x03<<12);//设置1个停止位
    

    接下来就要配置波特率了
    波特率的配置是由波特比率寄存器决定的

    DIV_Mantissa是USARTDIV的整数部分
    DIV_Fraction[3:0]:USARTDIV的小数部分
    计算方式为:
    72M/usartdiv/16=115200
    usartdiv = 72M/16/115200 = 39.0625
    39.0625 = DIV_Mantissa + (DIV_Fraction/16)
    DIV_Mantissa(整数部分) = 39
    DIV_Fraction(小数部分) = 0.0625*16 = 1;
    其中72M是由所在的时钟总线APB2决定的。
    APB2总线默认时钟频率与系统时钟频率保持一致是72MHz。

    此时,完整的串口1配置代码为:

    	//配置串口1    8数据位,0校验位,1停止位,波特率115200
    		USART1->CR1 &= ~(0x01<<12);//数据位置0
    		USART1->CR1 &= ~(0x01<<10);//设置0位校验位
    		USART1->CR2 &= ~(0x03<<12);//设置1个停止位
    		USART1->BRR = (39<<4)+1;//设置波特率
    		USART1->CR1 |= 0x01<<3;//发送使能
    	    USART1->CR1 |= 0x01<<13;//使能串口1
    

    此时的串口就完全配置好了,可以直接通过串口的数据寄存器发送一个8位的数据。

    USART1->DR = 'A';
    

    4.printf函数的重定向

    配置完串口后,此时想要使用printf函数进行调试,还是不行的。
    我们要将printf函数进行重定向,让打印的数据通过串口输出出来。
    printf函数的重定向在官方给的标准库中是存在的,只需打开官方例程即可找到。

    找到后复制过来即可:

    int fputc(int ch, FILE *f)
    {
    	//printf函数最终会跳转到这里来运行
    	while((USART1->SR&0x1<<6)==0);
    	//发送数据
    	USART1->DR = (uint8_t)ch;
      return ch;
    }
    
    

    复制完后记得在头文件中声明一下。
    完成以上步骤就可实现printf串口调试功能了。

    三、需求的实现

    需求均已实现,完整代码如下:
    main.c

    #include "stm32f10x.h"
    #include "led.h"
    #include "key.h"
    #include "delay.h"
    #include "usart.h"
    #include "stdio.h"
    
    int a,b,c,d;//LED灯的标志位
    
    int main()
    {
    	
        Led_Init();
        key_Init();
        Beep_Init();
    	Usart1_Config();
    		
        while(1)
        {
    				if(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)==1)//key1
    			{
    				Delay_nms(10);
    			    if(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)==1)
    				{
    				  while(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)==1)
    					{}
    					Led_Toggle(1);
    						a++;
    						if(a==2)
    						{
    							printf("LED1灯灭\r\n");
    							a=0;
    						}
    						else
    						{
    							printf("LED1灯亮\r\n");
    						}
    				}
    			}
    				if(GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_4)==0)//key2
    			{
    				Delay_nms(10);
    			    if(GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_4)==0)
    				{
    				  while(GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_4)==0)
    					{}
    					Led_Toggle(2);
    						b++;
    						if(b==2)
    						{
    							printf("LED2灯灭\r\n");
    							b=0;
    						}
    						else
    						{
    							printf("LED2灯亮\r\n");
    						}
    				}
    			}			
    			if(GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_5)==0)//key3
    			{
    				Delay_nms(10);
    			    if(GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_5)==0)
    				{
    				  while(GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_5)==0)
    					{}
    					Led_Toggle(3);
    						c++;
    						if(c==2)
    						{
    							printf("LED3灯灭\r\n");
    							c=0;
    						}
    						else
    						{
    							printf("LED3灯亮\r\n");
    						}
    				}
    			}			
    			if(GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_6)==0)//key4
    			{
    				Delay_nms(10);
    			    if(GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_6)==0)
    				{
    				  while(GPIO_ReadInputDataBit(GPIOC,GPIO_Pin_6)==0)
    					{}
    					Led_Toggle(4);
    						d++;
    						if(d==2)
    						{
    							printf("LED4灯灭\r\n");
    							d=0;
    						}
    						else
    						{
    							printf("LED4灯亮\r\n");
    						}
    				}
    			}			
        }
    }
    
    

    usart.c

    #include "usart.h"
    #include "stdio.h"
    
    void Usart1_Config()
    {
    	//开时钟:GPIOA,USART1
    		RCC->APB2ENR |= 0x01<<2;//GPIOA
    	  RCC->APB2ENR |= 0x01<<14;//USART1
    	//配置对应的IO口 PA9(tx):复用推挽 PA10(RX):浮空输入
    		GPIOA->CRH &= ~(0x0f<<4);//PA9清0
    		GPIOA->CRH |= 0x0B<<4;//设置成复用推挽输出
    		GPIOA->CRH &= ~(0x0f<<8);//PA10清0
    		GPIOA->CRH |= 0x04<<8;//设置成浮空
    	//配置串口1    8数据位,0校验位,1停止位,波特率115200
    		USART1->CR1 &= ~(0x01<<12);//数据位置0
    		USART1->CR1 &= ~(0x01<<10);//设置0位校验位
    		USART1->CR2 &= ~(0x03<<12);//设置1个停止位
    	  /*
        72M/usartdiv/16=115200
        usartdiv = 72M/16/115200 = 39.0625
        39.0625 =     DIV_Mantissa + (DIV_Fraction/16)
        DIV_Mantissa(整数部分) =  39
        DIV_Fraction(小数部分) = 0.0625*16 = 1;
        0000 0010 0111 0001
        */
    		USART1->BRR = (39<<4)+1;//设置波特率
    		USART1->CR1 |= 0x01<<3;//使能串口发送
    	  USART1->CR1 |= 0x01<<13;//使能串口1
    }
    
    void SendData(uint8_t data)
    {
    	while((USART1->SR&0x01<<6)==0){}//等待上次发送完成
    	USART1->DR = data;//发送数据
    }
    
    int fputc(int ch, FILE *f)
    {
    	//printf函数最终会跳转到这里来运行
    	while((USART1->SR&0x1<<6)==0);
    	//发送数据
    	USART1->DR = (uint8_t)ch;
      return ch;
    }
    
    

    usart.h

    #ifndef _USART_H_
    #define _USART_H_
    #include "stm32f10x.h"
    #include "stdio.h"
    
    void Usart1_Config();
    void SendData(uint8_t data);
    int fputc(int ch, FILE *f);
    
    
    #endif
    

    key.c

    #include "stm32f10x.h"
    
    void key_Init()
    {
    	//开时钟
    	RCC->APB2ENR |= 0x01<<4;//PC
    	RCC->APB2ENR |= 0x01<<2;//PA
    	//配置模式
    	GPIOC->CRL &=~(0X0F << 24);//PC6   key4
    	GPIOC->CRL |= 0X04 << 24;
      GPIOC->CRL &=~(0X0F << 20);//PC5   key3
    	GPIOC->CRL |= 0X04 << 20;
    	GPIOC->CRL &=~(0X0F << 16);//PC4   key2
    	GPIOC->CRL |= 0X04 << 16;
    	GPIOA->CRL &=~0X0F;//PA0   key1
    	GPIOA->CRL |= 0X04;
    	
    }
    
    int Get_Key_Val(void)
    {
    	int key_val = 0;
    	if(!!(GPIOA->IDR &(0X01 << 0))==1)
    		key_val = 1;
    	if(!!(GPIOC->IDR &(0X01 << 4))==0)
    		key_val = 2;
    	if(!!(GPIOC->IDR &(0X01 << 5))==0)
    		key_val = 3;
    	if(!!(GPIOC->IDR &(0X01 << 6))==0)
    		key_val = 4;
    	
    	return key_val;
    }
    

    key.h

    #ifndef _KEY_H_
    #define _KEY_H_
    
    void key_Init();
    int Get_Key_Val(void);
    
    #endif
    
    

    led.c

    #include "stm32f10x.h"
    
    void Led_Init()
    {
    	//配置好模式,然后全灭
    	//开APB2时钟
    	RCC->APB2ENR |= 0X01 << 6;
        //配置PE2--PE5为通用推挽输出
    	GPIOE->CRL &=~(0X0F << 20);//PE5
    	GPIOE->CRL |= 0X03 << 20;
    	GPIOE->CRL &=~(0X0F << 16);//PE4
    	GPIOE->CRL |= 0X03 << 16;
        GPIOE->CRL &=~(0X0F << 12);//PE3
    	GPIOE->CRL |= 0X03 << 12;
    	GPIOE->CRL &=~(0X0F << 8);//PE2
    	GPIOE->CRL |= 0X03 << 8;
    	//4个引脚均输出高电平
    	GPIOE->ODR |= (0x0F << 2);
    }
    //开关灯
    void Led1_Ctrl(int flag)
    {
      if(!!flag)
    	 {
    		GPIOE->ODR &= ~(0x0F << 2);
    	 }
    	else
    	 {
    		GPIOE->ODR |= (0x0F << 2);
    	 }
    	 
    }
    
    void Led_Toggle(int flag)
    {
    	GPIOE->ODR ^= 0x01<<(flag+1);
    }
    
    

    led.h

    #ifndef _LED_H_
    #define _LED_H_
    
    void Led_Init();
    void Led1_Ctrl(int flag);
    void Led_Toggle(int flag);
    
    #endif
    
    

    delay.c

    #include "stm32f10x.h"
    #include "delay.h"
    
    void Delay_nus(uint32_t time)
    {
        uint32_t i=0;
        for(i=0;i<time;i++){
            delay1us();
        }    
    }
    
    void Delay_nms(uint32_t time)
    {
        uint32_t i=0;
        for(i=0;i<time;i++){
             Delay_nus(1000);//延时1ms
        }    
    }
    
    

    delay.h

    #ifndef _DELAY_H_
    #define _DELAY_H_
    #include "stm32f10x.h"
    
    #define delay1us() {__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();__NOP();__NOP();}
    
    void Delay_nus(uint32_t time);
    void Delay_nms(uint32_t time);
    #endif
    		
    

    总结

    1.了解多种通信方式和作用。
    2.学会了如何在固件库中查找以及使用官方例程。
    3.学会了USART1的配置及使用,以后就可以对程序直接进行printf调试了。

    作者:小白橘颂

    物联沃分享整理
    物联沃-IOTWORD物联网 » STM32教程第三讲:串口调试技巧

    发表回复