从舵机程序到栈

    这是一个综合使用Keil中几项不同Debug功能的笔记。

    这两天在写一个舵机的驱动时遇到了引脚不能输出PWM的问题。开始以为是板子的问题——因为这是一个大二师妹画的,感觉新手出问题挺正常的。不过想到另一个同学在不久前才刚刚试用通过,后来还拿密封袋包着放起来了(画板子的师妹只焊了一块,所以比较<贵重>),所以应该不是硬件出了问题 - -。
    好吧出了问题还是从自己身上找原因比较好 - -。

0x00.实验资源

  • Windows7 & Keil for ARM v5.16
  • 一块有三路PWM输出的舵机控制板,主控STM32F103C8

0x01.舵机程序

int main(void)
{
    TIM1_GPIO();            //cwq-三路PWM引脚输出
    TIM1_Configuration();   //cwq-定时器1时基设置
    TIM1_PWM();             //cwq-定时器1的PWM功能设置
    TIM1_Start();           //cwq-PWM输出使能
    TIM1->CCR2 = 150;     
    TIM1->CCR3 = 130;
    TIM1->CCR1 = 80;        //cwq-改变占空比

    while(1);
}


void TIM1_Configuration(void)   //cwq-定时器1时基设置
{
    TIM_TimeBaseInitTypeDef  tim;
    NVIC_InitTypeDef         nvic;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1,ENABLE);

    nvic.NVIC_IRQChannel = TIM1_CC_IRQn;
    nvic.NVIC_IRQChannelPreemptionPriority = 1;
    nvic.NVIC_IRQChannelSubPriority = 1;
    nvic.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&nvic);           //cwq-打开捕获/比较中断。实际没用上

    tim.TIM_Prescaler = 72*10-1;    //APB2 is 72MHz cwq-分频系数作为频率的分母,系数越大,分得的频率越小,周期越长
    tim.TIM_CounterMode = TIM_CounterMode_Up;
    tim.TIM_ClockDivision = TIM_CKD_DIV4;
    tim.TIM_Period = 2000-1 ;      
    TIM_TimeBaseInit(TIM1,&tim);
}

void TIM1_GPIO(void)
{
    GPIO_InitTypeDef    gpio;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);

    gpio.GPIO_Mode = GPIO_Mode_AF_PP;
    gpio.GPIO_Pin = GPIO_Pin_8|GPIO_Pin_9|GPIO_Pin_10;
    gpio.GPIO_Speed = GPIO_Speed_50MHz;

    GPIO_Init(GPIOA,&gpio);
}

void TIM1_PWM()
{
    static TIM_OCInitTypeDef    oc;

    oc.TIM_OCMode = TIM_OCMode_PWM1;    
    oc.TIM_Pulse = 0;
    oc.TIM_OCPolarity = TIM_OCPolarity_High;
    oc.TIM_OutputState = TIM_OutputState_Enable;    

    TIM_OC1Init(TIM1,&oc);  
    TIM_OC2Init(TIM1,&oc);  
    TIM_OC3Init(TIM1,&oc);

    TIM_CtrlPWMOutputs(TIM1,ENABLE);    
    TIM_ARRPreloadConfig(TIM1, ENABLE);
}

void TIM1_Start(void)
{
    TIM_Cmd(TIM1, ENABLE);   
    TIM_ITConfig(TIM1, TIM_IT_Update,ENABLE);
    TIM_ClearFlag(TIM1, TIM_FLAG_Update);   
}

void TIM1_IRQHandler(void)
{
    if (TIM_GetITStatus(TIM1,TIM_IT_Update)!= RESET) 
    {
        TIM_ClearFlag(TIM1, TIM_FLAG_Update);   
    }
}

这乍一看,并没有什么问题,但在软仿中从引脚看不到波形:

TIM1_CH1\2\3

后来直接拿到测试过这块板子的同学的代码,通过比较,发现最大的区别在于她没有使用中断,而且<把初始化都写在了一个函数里面>。

下面是她的代码:

int main(void)
{
    TIM1_PWM_Init();
    PWM_OUT1(50);
    PWM_OUT2(180);
    PWM_OUT3(180);
    while(1);
}


void TIM1_PWM_Init(void)
{
    GPIO_InitTypeDef gpio;
    TIM_TimeBaseInitTypeDef tim;
    TIM_OCInitTypeDef oc;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_TIM1,ENABLE);

    gpio.GPIO_Pin=GPIO_Pin_8|GPIO_Pin_9|GPIO_Pin_10;
    gpio.GPIO_Mode=GPIO_Mode_AF_PP;
    gpio.GPIO_Speed=GPIO_Speed_50MHz;
    GPIO_Init(GPIOA,&gpio);

    tim.TIM_ClockDivision=TIM_CKD_DIV1;
    tim.TIM_CounterMode=TIM_CounterMode_Up;
    tim.TIM_Period=1999;
    tim.TIM_Prescaler=719;
    TIM_TimeBaseInit(TIM1,&tim);

    oc.TIM_OCMode=TIM_OCMode_PWM1;
    oc.TIM_OCPolarity=TIM_OCPolarity_High;
    oc.TIM_OutputState=ENABLE;
    oc.TIM_Pulse=0;

    TIM_OC1Init(TIM1,&oc);
    TIM_OC2Init(TIM1,&oc);
    TIM_OC3Init(TIM1,&oc);
    TIM_CtrlPWMOutputs(TIM1,ENABLE);
    TIM_ARRPreloadConfig(TIM1,ENABLE);

    TIM_Cmd(TIM1,ENABLE);
}

void PWM_OUT1(int pwm1)
{
    TIM1->CCR1=pwm1;
}

void PWM_OUT2(int pwm2)
{
    TIM1->CCR2=pwm2;
}

void PWM_OUT3(int pwm3)
{
    TIM1->CCR3=pwm3;
}

正常波形

很奇怪。虽然我只是把初始化分成几个子函数,还加上了一个没使能的中断——但就是不能正常工作。

一阵郁闷过后,突然想起<可以用SystemViewer对比两者的寄存器>,很大可能可以从那里找到原因。

0x02.从程序到SystemViewer

打开两个程序,分别进入Debbuger->View->SystemViewer->TIM->TIM1,打开TIM1的相关寄存器窗口,分别一个函数一个函数地执行,观察这些寄存器的变化过程(前提是调整这些函数的执行顺序相同)——发现有不同的变化时,就一个一个到手册里查。其中会很多寄存器是分频系数或者计数或者其它的普通的数据寄存器。

执行完TIM_TimeBaseInit(TIM1,&tim)函数,两个代码的寄存器就出现了不同:

SystemViewer1

从手册查得:

中文手册的说明

这段话我没懂,只是发现寄存器DMAR和CR1有点关系。手动修改CR1的值,DMAR的值也会相应改变。说明DMAR的值只是受CR1影响。而至于两边的CR1里不同的数值,是一个分频因子,所以这个函数的分析可以暂时通过了。
继续运行,执行完TIM_OC1Init(TIM1,&oc)后,CR2和CCER出现不同值:

SystemViewer

从手册查得:

SystemViewer4
SystemViewer5

这两个寄存器说明我已经设置了通道2\3\4的一些参数……咦?不对啊,我只设置了通道1啊,因为只执行了TIM_OC1Init(TIM1,&oc)这一个函数。如果说这些位是因为某种原因被错误设置的话,在初始化时结构体成员应该被错误设置了。

0x03.从SystemViewer到assert_failed()

要看库函数参数设置得对不对,可以用一个assert_param()函数来检查。如下图:

assert1

虽然每个库函数默认都使用这个函数来检查引入的实参是否满足要求,但官方也默认这个函数是空的,也就是:即使检查出实参不符合要求,也不会进行处理。

assert2

这里使用了<条件编译>,如果在此处之前<有宏定义 USE_FULL_ASSERT>,那么参数被检查到出错后,会转而执行assert_failed()函数;否则assert_param()为空函数,没有参数检查的功能。ST官方<没有宏定义 USE_FULL_ASSERT>.
另外,assert_failed()并没有进行定义。在这里ST官方只是为了提示用户<有这么一种方法检查参数哦>才给了这一个assert_failed((uint8_t *)__FILE__, __LINE__))
那作为用户的我们应该怎么做呢?我对C语言只是略懂皮毛,所以很粗糙地在main.c里面加了个:

void assert_failed(uint8_t* file, uint32_t line)
{
      while(1);
}

在工程option->C/C++-里面<添加宏定义USE_FULL_ASSERT>,然后随便在一个头文件声明一下就好了~
接下来就是检查一下有没有出错的参数了——在Debugger里给TIM_OC1Init(TIM1,&oc)添加一堆断点——如果有哪个断点是不能到达的,就说明程序掉到aseert_failed()里了,至少可以确定前一行的参数出了问题。嘻嘻。

按照这个思路,果然找到了出错的三个参数:

assert3

啊哈!加上中间没错的那行,就是我没有设置的那四个结构体成员。嗯——接下来试试补上这四句代码:

    oc.TIM_OCIdleState = TIM_OCIdleState_Reset;
    oc.TIM_OCNIdleState = TIM_OCNIdleState_Reset;
    oc.TIM_OCNPolarity = TIM_OCNPolarity_High;
    oc.TIM_OutputNState = TIM_OutputNState_Disable;

就这样,编译过后再进入LA,竟然可以看到PWM输出了:

assert4

0x04.从assert_failed()到TIM1特点

为什么给增加四个成员设置就能正常看到PWM了呢?换句话说,为什么这四句代码能影响TIM1的正常工作呢?

从参考手册可以看到,TIM1和TIM8一样,都是<高级定时器>,其它几个都是<通用定时器>。另外大容量型号的STM32F103还有两个<基本定时器>。这个TIM1,作为<高级定时器>,最大的特点是具有<带死区的PWM互补输出>。我想这个是专为电机半桥和H桥设计的,从结构体成员名就可以看出来。后面没有深究。

<高级定时器>简述

尽管库函数使用手册里面有提到这四个成员是TIM1和TIM8专用的,但没有说这是必要的

TIM_OCInitTypeDef

0x05.从TIM1特点到MemoryWindows

既然是一个没必要的设置,为什么在这里变得必要了?——缺少这四句代码,初始化函数的参数检查就不能通过,导致PWM不能正常输出——对了,参数不能通过。也许是它们的复位值本身就不能通过参数检查?或者是前面的步骤使得它们的复位值改变了?要想验证这些想法,MemoryWindows说不定能派上用场。

重新贴一下我的舵机代码(去掉没用的中断):

int main(void)
{
    TIM1_GPIO();            //cwq-三路PWM引脚输出
    TIM1_Configuration();   //cwq-定时器1时基设置
    TIM1_PWM();             //cwq-定时器1的PWM功能设置
    TIM1_Start();           //cwq-PWM输出使能
    TIM1->CCR2 = 150;     
    TIM1->CCR3 = 130;
    TIM1->CCR1 = 80;        //cwq-改变占空比

    while(1);
}


void TIM1_Configuration(void)   //cwq-定时器1时基设置
{
    TIM_TimeBaseInitTypeDef  tim;
    NVIC_InitTypeDef         nvic;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1,ENABLE);

    nvic.NVIC_IRQChannel = TIM1_CC_IRQn;
    nvic.NVIC_IRQChannelPreemptionPriority = 1;
    nvic.NVIC_IRQChannelSubPriority = 1;
    nvic.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&nvic);           //cwq-打开捕获/比较中断。实际没用上

    tim.TIM_Prescaler = 72*10-1;    //APB2 is 72MHz cwq-分频系数作为频率的分母,系数越大,分得的频率越小,周期越长
    tim.TIM_CounterMode = TIM_CounterMode_Up;
    tim.TIM_ClockDivision = TIM_CKD_DIV4;
    tim.TIM_Period = 2000-1 ;      
    TIM_TimeBaseInit(TIM1,&tim);
}

void TIM1_GPIO(void)
{
    GPIO_InitTypeDef    gpio;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);

    gpio.GPIO_Mode = GPIO_Mode_AF_PP;
    gpio.GPIO_Pin = GPIO_Pin_8|GPIO_Pin_9|GPIO_Pin_10;
    gpio.GPIO_Speed = GPIO_Speed_50MHz;

    GPIO_Init(GPIOA,&gpio);
}

void TIM1_PWM()
{
    static TIM_OCInitTypeDef    oc;

    oc.TIM_OCMode = TIM_OCMode_PWM1;    
    oc.TIM_Pulse = 0;
    oc.TIM_OCPolarity = TIM_OCPolarity_High;
    oc.TIM_OutputState = TIM_OutputState_Enable;    
    oc.TIM_OCIdleState = TIM_OCIdleState_Reset;   //cwq-以下四个就是出错时忽略的成员
    oc.TIM_OCNIdleState = TIM_OCNIdleState_Reset;
    oc.TIM_OCNPolarity = TIM_OCNPolarity_High;
    oc.TIM_OutputNState = TIM_OutputNState_Disable;

    TIM_OC1Init(TIM1,&oc);  
    TIM_OC2Init(TIM1,&oc);  
    TIM_OC3Init(TIM1,&oc);

    TIM_CtrlPWMOutputs(TIM1,ENABLE);    
    TIM_ARRPreloadConfig(TIM1, ENABLE);
}

void TIM1_Start(void)
{
    TIM_Cmd(TIM1, ENABLE);   
    TIM_ITConfig(TIM1, TIM_IT_Update,ENABLE);
    TIM_ClearFlag(TIM1, TIM_FLAG_Update);   
}

void TIM1_IRQHandler(void)
{
    if (TIM_GetITStatus(TIM1,TIM_IT_Update)!= RESET) 
    {
        TIM_ClearFlag(TIM1, TIM_FLAG_Update);   
    }
}

我在TIM1_PWM()中定义oc的下一行设置断点1,在TIM1_PWM()中oc成员初始化完成后设置断点2。当程序执行到断点暂停时,断点所在行<还没开始>执行,但它的上方程序都已经执行完毕。两个断点设置如图:

断点设置

执行到断点1之后,oc被创建,在MemoryWindows输入oc可以看到它的初始值。到断点2,oc成员(每个都是16位长度)被全部设置。如下图:

断点1-oc初始化值

断点2-出错时oc的设置值

去掉注释重新编译,执行,如下图:

断点1-oc初始化值

断点2-出错时oc的设置值

说明:绿色表示创建后至少访问过一次的地址,金色表示创建后未被使用过的地址,黑色表示没有用到(创建)的地址。另外,尽管STM32是32位单片机,地址总线和数据总线的宽度都是32位,但是每个地址都只指向一个字节而不是四个字节——这让人感觉很浪费。不过这样应该有它的道理。

好了,通过MemoryWindows可以看到,oc某些成员的值确实会因为代码的缺失而<没有被正确设置>,而不是<复位到了错误的值>。

那么,<没有被正确设置>的这些成员用的这些<非零值>是从哪里来的呢?

复位为零值

在写这篇博客的过程中,程序几经修改,使得原来的<没有PWM输出的错误>不能重现了 - -,换成了一个新的

0x06.从MemoryWindows到CM3存储器映像

<内存地址的使用是存在一定的分配规则>,这在之前学习单片机的时候留下的印象。

从参考手册中找了一下,有一个存储器映像说明:

cwq-CM3存储器映像1

里面没有0x2000xxxx地址的说明。

搜索<0x2000>,原来属于SRAM的范围:

cwq-CM3存储器映像2

也就是说,从0x20000000开始,都是RAM的范围(S表示静态,充电一次后每一位都不会漏电,无须重新充电。于此对应的是DRAM,需要不断充电刷新,否则会漏电造成数据丢失。两者的根本区别在于结构:SRAM有6个电容而DRAM只有2个。扯远了 - -)那为什么不从0x20000000开始向后而要从0x20000438开始向前?

几经搜索,找到了正点原子论坛的一个非~常详细的讲解帖子。大部分内容看的不是很懂,从中得到一个很大的提示:<去.map文件可以查看代码的内存使用情况>:

cwq-stack2

表示0x20000438是<栈>的<栈顶指针>,初始化占用0个字节——也就是指向栈底。

startup_stm32f10x_md.s文件的前面,也可以找到关于<栈>的信息:

cwq-stack3

表示<栈大小>为<0x00000400>个字节。一般来说,内存中的<栈>是向着地址减小的方向生长的。那么由:<0x20000438-0x00000400=0x20000038>约等于0x20000000,RAM的起始地址,就大约可以解释为什么<局部变量>的定义是从0x20000438开始了。

而0x20000000到0x20000038的分配情况也可以从这里看到:

cwq-stack4

上面的Base和Max和工程设置或者startup文件有关:

cwq-stack1

左边是ROM(不知道和flash有什么联系 - -lll),右边是RAM

0x07.从存储器映像到栈的使用

在内存中的栈是用来存放子函数的<局部变量>的,<局部变量>压栈之前还要一些<现场保护>措施——保存cpu内部几个寄存器的值,包括主堆栈指针MSP\进程堆栈指针PSP\连接寄存器R14\程序计数器R15等

0x08.结论

要解决最初那个PWM输出不正常的问题,有两个方法:
- 在执行初始化函数之前,把全部结构体成员初始化。这样就不会把内存之前留下的旧值错误地赋值到寄存器
- 把会出错的这个结构体作为<全局变量>或者<静态局部变量>,这样就不会使用到栈了。即使没有初始化全部成员也不用担心出现错误——虽然还是会带入零值作为未初始化,但这刚好是绝大部分寄存器的复位值
- 这个第三种方法是在定义变量时使用__attribute__()命令指定它使用的地址。我不会用。ARM的在线文档可以搜到这个命令的简介

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值