【学习笔记】STM32hal库开发入门笔记

前言

这是一个大概半年前的自学笔记,现在整理放到博客上来,笔记还未完善甚至还有一些内容因为还没学到直接空了出来,以后会随着学习继续完善,如果真有大佬看完的话十分欢迎大佬指教

找资料

野火,正点原子,硬石

学习途径

野火,正点原子,韦东山,b站Just_Hello_World、WUT_电子科协。

两种手册

参考手册:

片上外设工能说明和寄存器描述。编程时常查看。

数据手册:

功能概览,引脚说明,内存映射和封装特性。

开发步骤

keil工程模板

这个工程模板可以自己做,也可以用现有的修改或者cubeMX生成。
[(media/0adbb536cbf30c1b60e6ec1d77e45746.png)]

这些都是根据工程师习惯设置的,并不是一定的,就像行业的不成文的规定吧。

配置Core文件夹


启动文件,找到自己开发的对应的芯片,移植过去。


还有这两个。(应该也要分芯片吧)


这两个。


这两个一般是根据自己的需求配置的时钟文件,当然也可以借鉴别人的模板。

it.c和it.h是关于中断的文件,system是系统初始化的文件,hal_conf是hal库的配置文件,可以配置一些外设,启动或者不启动。

SDK文件夹

选择芯片

把工程文件做成模板的格式


点击这个

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6D1WkKf1-1647246825921)(media/aae57574701205895290d4ec86387198.png)]
target这里改为工程的名字,groups这里新建出模板对应的各个文件



给每个组添加应该对应的文件,这就是之前做的那些文件夹的意义了。core一般添加这些,SDK的话图中除了gpio其他的基本就是必备的,剩下的就看需要用到哪些模块就添加哪些文件。

添加文件也可以通过双击右侧的组然后进行添加。main一般就是新建之后保存了再添加进去。

设置keil

(media/1fa9429217aba8ed95ce27b3099e36c8.png)]

在C/C++选项里面设置必要的包含路径,就是放一些库的源码啊啥的。添加这些文件夹的路径。

定义宏


比如这里用到了HAL库,所以定义了一个HAL库的宏,还定义了一个STM32F103xE的宏,中间用逗号分隔开。

在target选型设置编译器,


选择版本5。


回到C/C++选项勾选C99模式,因为hal库使用到了C99的语法。


Debug选项可以选择使用的调试方式

点进setting后,如果调试器已经连接上板子能并连上芯片的话,就能显示出芯片的id和设备信息。

连接上之后就可以开始调试代码了。调试过程可以看到基础寄存器、变量和内存等。

调试过程中还可以在这里查看外设。看到外设的寄存器的值然后跟手册对比就可以知道外设处于什么状态。

番外之调教IDE

Edit配置configuration

这里可以设置一些编辑器的快捷键还有字体、风格和字库编码等等。

CubeMX建立模板

首先选好芯片,然后进入后配置时钟源、时钟树和其他各项需要的功能,然后命名工程名字和选择工程保存的位置,最后即可生成工程。

时钟配置:

分高速/低速和内部/外部晶振,时钟树配置的时候要对应。

时钟树:

可以直接输入HCLK需要配置的值(一般配到能接受的最大值),摁下回车软件能帮你自己配好(要是人工智障了就自己配吧)。

工程文件生成设置:

  • 文件名和保存地址。
  • 对应的IDE
  • 堆栈的大小,一般不用管,搞USB的时候可能要改大点
  • cubeMX的固件包选择。有时候在因为墙的问题软件下不下来就自己找个资源下载后再添加本地的吧。
  • https://blog.csdn.net/as480133937/article/details/98947162
    更详细看这里。

    配置CubeMX时看不懂那些配置选项就看用户手册,基本就都是那些寄存器位。

    小问题1-debug设置

    有个小问题要注意:debug这里如果选了No Debug就会导致SWDIO和SWCLK没有配置,下一次下载会无法烧录。如果已经这样了就需要重新配置boot引脚电平,boot0->1,boot1->0,进入内部存储启动模式,通过
    串口
    对芯片烧录或者擦除原来的程序。或者摁着reset,点擦除或者烧录,然后瞬间放开reset,这个考验手速。

    如果要直接看初始配置代码,可以在文件 stm32f1xx_hal_msp.c 中:

    __HAL_AFIO_REMAP_SWJ_DISABLE(); //SWD和JLINK都不支持

    __HAL_AFIO_REMAP_SWJ_ENABLE();
    //全功能的JLINK功能(当然包括SWD功能,即ST-LINK也是支持的)

    __HAL_AFIO_REMAP_SWJ_NOJTAG(); //支持SWD,即ST-LINK可用,但不支持JLINK

    据说,也可以ST_LINK接上rst线进行烧录:对于标准的ST-LINK接口,实际上是5根线:VCC,GND,
    SWDIO, SWCLK,
    nRST。最后一个是复位信号,跟MCU的RESET管脚相连接,但大家实际使用时,一般都是用前4根线,省掉了复位线,大多数情况下这样使用也是没有问题的,因为SWD调试协议里,通过数据线SWDIO的特殊数据序列可以“”软复位“MCU,然后进入debug状态。但现在的情况是,MCU一上电就立即运行代码、禁用了debug(如果启动代码中配置了SWDIO管脚为输入输出功能,也会是类似的情况),来自SWDIO的软复位来不及动作,实际上就是ST-LINK未能正确reset
    MCU。为了使ST-LINK能reset MCU,这时第5根线nRST就派上用场了。

    然后配置connect选项为under reset。

    当项目中MCU选型使用TSOP封装等仅有少量pin的芯片时,可能必须使用调试管脚作为GPIO来使用,为了不影响debug和程序下载,可以考虑下面的变通方法:

    调试代码时,先把SWDIO、SWCLK等pin配置成debug模式,即:

    __HAL_AFIO_REMAP_SWJ_ENABLE();

    这种情况下,可以正常调试程序,当然,对应管脚的GPIO功能是无效的;当整体功能调试完毕、确认不再需要调试功能后,再把SWDIO、SWCLK配置成GPIO模式。此时,使用5线制的ST-LINK(带复位线)是可以正常下载的(因为复位后MCU默认是支持debug的,只要之后不运行用户程序,可debug状态就不改变),但不能debug。

    小问题2-黄色感叹号

    因为有很多引脚可以有多种功能可以选择设置,黄色感叹号表示这一块里面的某个功能不能使用了,因为引脚已经被占用了,而粉色标示出来的就是不能用的功能。

    代码细说

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7xptg10F-1647246825927)(media/1d786f0801889a512d812d7fe2d4b9f1.png)]

    生成的工程就如上图一样的模板。

    就是启动(startup)文件那里。(汇编文件)

    函数前的注释语句:@brief
    说明函数作用,@param说明参数,@retval说明函数返回值,HAL库的函数返回值一般都是枚举型,可以直接去右键goto查看

    Hal库GPIO

    初始化

    细嗦初始化函数源码:

    void MX_GPIO_Init(void)

    {

    GPIO_InitTypeDef GPIO_InitStruct = {0};

    /* GPIO端口时钟使能 */

    __HAL_RCC_GPIOC_CLK_ENABLE();

    __HAL_RCC_GPIOA_CLK_ENABLE();

    __HAL_RCC_GPIOB_CLK_ENABLE();

    /*配置GPIO端口引脚的初始化输出电平 ,引脚为PB8,PB9,输出电平为低电平*/

    HAL_GPIO_WritePin(GPIOB, GPIO_PIN_8|GPIO_PIN_9, GPIO_PIN_RESET);

    /*配置GPIO端口输入引脚 : PC13 */

    GPIO_InitStruct.Pin = GPIO_PIN_13; //GPIO端口的引脚号是:13

    GPIO_InitStruct.Mode = GPIO_MODE_INPUT; //GPIO的模式是:输入

    GPIO_InitStruct.Pull = GPIO_NOPULL; //没有上拉

    HAL_GPIO_Init(GPIOC, &GPIO_InitStruct); //将参数结构设置到GPIOC端口

    /*配置GPIO端口输出引脚 : PB8 PB9 */

    GPIO_InitStruct.Pin = GPIO_PIN_8|GPIO_PIN_9; //GPIO端口的引脚号是:8和9

    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; //GPIO的模式是:输出

    GPIO_InitStruct.Pull = GPIO_NOPULL; //没有上拉

    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; //GPIO的输出速度是:非常低速

    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct); //将参数结构设置到GPIOB端口

    }

    原文链接:https://blog.csdn.net/weixin_46301543/article/details/122449943

    部分相关函数

    void HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init);

    功能: GPIO初始化

    实例:HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);

    void HAL_GPIO_DeInit(GPIO_TypeDef *GPIOx, uint32_t GPIO_Pin);

    功能:在函数初始化之后的引脚恢复成默认的状态,即各个寄存器复位时的值

    实例:HAL_GPIO_Init(GPIOC, GPIO_PIN_4);

    GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);

    功能:读取引脚的电平状态、函数返回值为0或1

    实例:HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_4);

    void HAL_GPIO_WritePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, GPIO_PinState
    PinState);

    功能:引脚写0或1

    参数1:GPIOx,端口号,如:GPIOB,GPIOF。

    参数2:GPIO_Pin,引脚号,如:GPIO_PIN_9,GPIO_PIN_12。

    参数3:PinState,引脚输出状态。高电平—-GPIO_PIN_SET;低电平—-GPIO_PIN_RESET。

    实例:HAL_GPIO_WritePin(GPIOC, GPIO_PIN_4,GPIO_PIN_SET);

    void HAL_GPIO_TogglePin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);

    翻转引脚的电平状态

    实例:HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_4); 常用在LED上

    HAL_StatusTypeDef HAL_GPIO_LockPin(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);

    功能:锁住引脚电平,比如说一个管脚的当前状态是1,当这个管脚电平变化时保持锁定时的值。

    实例:HAL_GPIO_LockPin(GPIOC, GPIO_PIN_4);

    void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin);

    功能: 外部中断服务函数,清除中断标志位

    实例:HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_4);

    void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin);

    功能: 中断回调函数,可以理解为中断函数具体要响应的动作。

    实例:HAL_GPIO_EXTI_Callback(GPIO_PIN_4);

    一些输入输出的硬件结构

    开漏输出:

    只能输出低电平(接地),或者悬空(对外输出高电平要像上图中外接电源,实现了单片机所不能的高负载驱动电压电流)。并且可以实现逻辑与功能(多个开漏接在一个线路上)。


    推挽输出:

    两个互补的MOS管,控制这两个实现输出的高低电平(VDD/VSS)。

    上/下拉模式:

    上开关闭合则为上拉输入模式,外界只有接入低电平时才会输入低,否则悬空时输入也是高。下开关闭合则为下拉输入模式,外界只有接入高电平时才会输入低,悬空时输入也是低。

    浮空输入模式:

    上下开关都断开,但是有一个TTL二极管,输入只能是0/1,输入取决于外部输入是什么,外界没有输入的时候输入就不确定了。

    gpio设置输出速度(即IO口相应速度),速度越高抗干扰越弱功耗越大噪声也越大,一般通信(IIC、SPI)用高速,驱动蜂鸣器LED之类的低速。

    CubeMX设置GPIO引脚

    output leval:初始化后的输出

    mode:推挽输出还是开漏输出

    pull-up/down:上下拉

    Maximum output speed:引脚速度

    User label:可以用这个label指代这个引脚(就是宏定义啦)

    外部按键中断也是再GPIO设置。可以设置上升下降沿触发。

    GPIO中断要开外部总线中断。

    软件默认的中断设置中system tick
    timer的优先级跟外部中断的优先级是一样的,这样就导致在外部中断中用HAL的延时函数会使单片机卡死,所以要自己去降外部中断的优先级。

    GPIO外部中断回调函数:HAL_GPIO_EXIT_Callback(uint16_t GPIO_Pin)

    例子:

    时钟

    时钟树

    系统时钟:

    可以通过内部高速时钟直接得到也可以分频再通过PLL倍频得到,或者,可以通过外部高速时钟直接得到也可以经过分频再倍频也可以直接倍频。(总之看图,条条大路)。倍频最多16倍,所以系统时钟为了达到最高72一般用外部配合倍频得到。

    内部低速用作看门狗使用。

    外部低速时钟,还不太懂。(和外部同步用之类的吧)

    时钟函数库

    晶振初始化结构体:RCC_OscInitTypeDef

    描述的是内外部的晶振类型状态之类的和PLL的设置。

    typedef struct

    {

    uint32_t OscillatorType; //选定将被配置的振荡器

    uint32_t HSEState; //HSE状态

    uint32_t LSEState; //LSE状态

    uint32_t HSIState; //HSI状态

    uint32_t HSICalibrationValue; //HSI校准调整值

    uint32_t LSIState; //LSI状态

    #if defined(RCC_HSI48_SUPPORT)

    uint32_t HSI48State; //HSI状态,#if defined(RCC_HSI48_SUPPORT)

    #endif

    uint32_t MSIState; //MSI状态

    uint32_t MSICalibrationValue; //MSI校准调整值

    uint32_t MSIClockRange; //MSI频率范围

    RCC_PLLInitTypeDef PLL; //PLL结构体参数

    } RCC_OscInitTypeDef;

    这个结构体配置的是这一层,比如是打开还是关闭哪个时钟,选择内部还是外部时钟,选择分频倍频的值,最后得到系统时钟。

    RCC初始化结构体:RCC_ClkInitTypeDef

    typedef struct

    {

    uint32_t ClockType //选定将被配置的时钟

    uint32_t SYSCLKSource //系统时钟来源(内部高速/外部高速)

    uint32_t AHBCLKDivider //配置AHB(HCLK)的时钟预分频,该时钟由SYSCLK而来

    uint32_t APB1CLKDivider // APB1时钟(PCLK1)分频器,该时钟由HCLK而来

    uint32_t APB2CLKDivider // APB2时钟(PCLK2)分频器,该时钟由HCLK而来

    } RCC_ClkInitTypeDef;

    就是绿圈这一块。

    RCC常用操作函数

    前三个和圈起来的比较常用,

    这些函数的查看可以在左边的导航栏里的functions查看。


    HAL_RCC_OscConfig中的结构体如上。


    结构体中的这些成员基本都是枚举的,比如OscillatorType就是RCC_Oscillator_Type中的一个。使用这些成员时可以查阅这些枚举的值以及对应的含义可以通过选中查找的方式(工程内查找)。

    CubeMX设置时钟


    按照自己的板子实际情况设置好左下角的input
    fequency,再输入需要的HCLK,直接回车就可以自动配置好其他的,如果不行就自己手动配置分频倍频咯。

    定时器

    定时器配置

    模式设置

    时钟源设置:

    内部时钟(TIMx_CLK)

    外部时钟模式1:外部捕捉比较引脚(TIx)

    外部时钟模式2:外部引脚输入(TIMx_ETR) 仅适用TIM2,3,4

    内部触发输入(ITRx):使用一个定时器作为另一个定时器的预分频器,如可以配置一个定时器Timer1而作为另一个定时器Timer2的预分频器

    只用作普通定时器做中断的话,时钟源选择内部时钟,然后函数中定义中断回调函数即可。

    如果需要定时器联动起来的话,需要设置主从定时器,就是主定时器就跟上图一样,

    从定时器就需要选择slave mode和trigger source

    Slave mode:

    External Clock Mode1——外部时钟:使用外部时钟源作为定时器时钟

    Reset Mode——复位模式:什么都不设置

    Gated Mode——门控模式:由主定时器电平高低来控制是否工作

    TriggerMode——触发模式:由主定时器信号变化触发是否工作

    从上面亦可以看出,从模式也未必需要和别的定时器联动起来,本质上来说从模式是意味着这个定时器的参数不再受mcu控制,而是受到外设控制,通过各种触发信号实现从定时器的设置。

    trigger source:

    这个从参考手册里面找,不同的两个定时器之间的连接的ITRx不一样,

    在“从模式控制寄存器(TIMx slave mode control
    register)”里可以找到。左边一列为从定时器,右边为对应的主。

    里面的通道选择(没学完)就是可以像理解示波器一样,可以同时捕获多个通道的信号,或者输出多路信号(一般是pwm)

    输入捕获:

    测量输入信号的脉冲长度或者周期频率啥的,就是跳变沿记录1次计数器的值,再次跳变沿再记录,算差值得出时间。由于定时器的计数器最多只有16位,故当周期过大时在测量过程中会出现定时器重载现象导致测量的值错乱。可以通过联动定时器的溢出中断解决这个问题,只需要把溢出次数记录下来改变一下差值的计算方法即可。

    捕获模式也分主从(dirrect/indirect)。

    输出比较:

    单脉冲模式输出

    可以发出1个脉宽可控的脉冲,发生1次溢出后,计数器停止计数。单脉冲的制造类似于pwm的那种方式。

    PWM输出(有边缘或中间对齐模式)

    设置个阈值,计数器过了那个值后输出从高电平变为低电平,从而实现PWM占空比控制。

    触发DMA:

    (6条消息)
    STM32-一文搞懂通用定时器捕获/比较通道_BUG从入门到精通-CSDN博客_定时器捕获与比较模式

    参数配置

    Counter settings

    Prtscaler (定时器预分频系数) :16bits 降低频率的(除法,比如72M/72=1M)

    Counter Mode(计数模式) :

    递增计数模式:计数器从 0 计数到自动重载值,然后重新从 0开始计数并生成计数器上溢事件。

    递减计数模式:计数器从自动重载值开始递减到0,然后重新从自动重载值开始计数并生成计数器下溢事件。

    中心对齐模式:计数器从 0 开始计数到自动重载值–1,生成计数器上溢事件;然后从自动重载值开始向下计数到 1 并生成计数器下溢事件。之后从0 开始重新计数。

    Counter Period(自动重装载值) :
    16bits(所以最大65535),定时用的,就是一直累加直到到那个值就发生溢出并重载

    CKD(时钟分频因子) : No Division 不分频 (可以选择2分频和4分频)

    repetition counter(重复计数):8bits
    高级寄存器才有,可以选择发生多少次溢出才会触发事件。

    auto-reload-preload(自动重装载) : Enable/disable(一个有缓存,一个没有),

    Disable:自动重装载寄存器写入新值后,计数器立即产生计数溢出,然后开始新的计数周期

    Enable:自动重装载寄存器写入新值后,计数器完成当前旧的计数后,再开始新的计数周期

    注意:prescaler、counter period、repetitioncounter这三个值算出来后要减1,因为是从0开始算的。

    TRGO(定时器的触发信号输出
    在定时器的定时时间到达的时候输出一个信号,主从定时器就要用到)

    是否使能;

    选择触发事件;


    本质上就是配置CR2寄存器的MMS(master mode select),以下来自参考手册

    MMS[2:0]:主模式选择 (Master mode selection)
    这3位用于选择在主模式下送到从定时器的同步信息(TRGO)。可能的组合如下:

    000:复位 – TIMx_EGR寄存器的UG位被用于作为触发输出(TRGO)。如果是触发输入产生的
    复位(从模式控制器处于复位模式),则TRGO上的信号相对实际的复位会有一个延迟。

    001:使能 – 计数器使能信号CNT_EN被用于作为触发输出(TRGO)。有时需要在同一时间启动
    多个定时器或控制在一段时间内使能从定时器。计数器使能信号是通过CEN控制位和门控模式
    下的触发输入信号的逻辑或产生。
    当计数器使能信号受控于触发输入时,TRGO上会有一个延迟,除非选择了主/从模式(见
    TIMx_SMCR寄存器中MSM位的描述)。

    010:更新 –
    更新事件被选为触发输入(TRGO)。例如,一个主定时器的时钟可以被用作一个从
    定时器的预分频器。

    011:比较脉冲 – 在发生一次捕获或一次比较成功时,当要设置CC1IF标志时(即使它已经为
    高),触发输出送出一个正脉冲(TRGO)。

    100:比较 – OC1REF信号被用于作为触发输出(TRGO)。

    101:比较 – OC2REF信号被用于作为触发输出(TRGO)。

    110:比较 – OC3REF信号被用于作为触发输出(TRGO)。

    111:比较 – OC4REF信号被用于作为触发输出(TRGO)。

    如果MMS[2:0]值为000:当TIMx_EGR寄存器的UG位有效,就会触发TRGO输出。

    如果MMS[2:0]值为010:当产生更新事件时,就会触发TRGO输出,从定时开启

    重复计数寄存器

    Repetition
    counter是一个STM32增强的计数器功能,有很多用途,RCR只在部分高级的定时器才有,

    假如,定时了0.001s,然后在中断中计数1000次,点亮熄灭LED,正常情况来说,led会亮1s,然后灭1s….不断重复。

    当 TIM_RepetitionCounter 参数设置为0
    时,确实是1s,当设置为1时就成了2s,2时成了3s。

    可以理解为定时器溢出再加了一个倍率控制如果是定时1S,那么设定N为1,那么就是倍率就是N+1=2倍。(大概也是从0算起)

    RCR寄存器中的值会递减到0,在允许更新事件UEV发生的情况下,则TIM的更新事件UEV就会产生;如果设置RCR的值为N,那么PWM模式下,更新事件将会在第N+1个周期发生。

    参考:(2条消息) STM32 – 定时器的设定 – 基础 01.1 – Repetition
    counter_山云的专栏-CSDN博客_repetitioncounter

    高级定时器的中断模式

    1. 没懂

    2. 更新中断模式,产生溢出时会跳转到中断服务函数。

    3. 没懂

    4. 没懂

    定时器函数

    定时器开关函数

    HAL_TIM_Base_Start:开启定时器,打开他的计数功能,计数中关闭计数器就会维持那个值

    HAL_TIM_ Base_Start _IT:开启定时器中断,计时器溢出更新时能产生中断

    关对应Stop。

    __HAL_TIM_SET_COUNTER(&htimx,0):这个可以直接设置计数器的值,但要在关闭后才可以。

    中断回调函数

    HAL_TIM_PeriodElapsedCallback

    在使用cubeMX时会自动生成一个在tim.c中(好像是没啥用的,就是给用户查看,也可能我还没搞懂),但是是有weak前缀修饰的,如果用户需要使用回调函数可以在用户文件中自己定义这个函数,以用户定义的为优先。

    关于这个函数的自己定义,就复制过来定义就好了(入参是没法改的),然后传入参数的指针就是用来判断时哪个中断。例子如下:

    小发现:

    回调中好像不能开关定时器,但是可以开关定时器中断。(计数还在进行)

    IT可能用的是取反的方式置位寄存器,Start和IT两个不论顺序连着开反而没开出中断。

    分割线2022.3.7————————————————————————

    后期使用捕获比较中断的时候又可以在回调函数中关了,很玄学。这次的发现是HAL_TIM_IC_Stop_IT就能把计数停止了。然后又试了试溢出中断中关溢出中断,计数居然也停止了?!现在的理解是在中断回调中关对应中断可以。

    再次尝试之前的代码,发现使用停止中断函数后计数就停止了。反而是base_stop这个函数在回调里不起效。

    更多请看库文件的描述

    可以参考(5条消息) STM32 库函数学习
    TIM篇_s2014201506的博客-CSDN博客_htim2.init.prescaler

    中断系统

    优先级设置

    优先级越小越流批。

    优先级有抢占优先级和子优先级,抢占优先级决定能不能在处理一个中断的时候打断这个中断,而子优先级只能在两个抢占优先级一样的中断
    同时
    发生的时候先执行那一个,就是说如果子优先级较弱的中断已经在执行了那么比它强的子优先级这个时候也只能等着。

    这两优先级一共只有4位,两个分配位数从而决定各自有多少级,也就是说两个优先级加起来一共刚好有16个组合(即:最多16个抢占没有响应,或者8*2,4*4,2*8,没有抢占16响应)。

    ADC

    基础概述

    STM32f103系列有3个ADC,精度为12位(即采集可以获得2^12的分辨率),每个ADC最多有16个外部通道。其中ADC1和ADC2都有16个外部通道,ADC3一般有8个外部通道,各通道的A/D转换可以单次、连续、扫描或间断执行,ADC转换的结果可以左对齐或右对齐储存在16位数据寄存器中。ADC的输入时钟不得超过14MHz(一般用12M,因为设置系统的时钟为72),其时钟频率由PCLK2分频产生。


    片内ADC的VREF- 和VSS接地,VREF+和 VDD 接
    3V3,所以测量电压范围为0—3.3V。如果要测更大自己在外面做电路或者用ADC模块。

    通道模式

    规则通道:

    最平常的通道、也是最常用的通道,平时的ADC转换都是用规则通道实现的。

    注入通道:

    注入通道是相对于规则通道的,注入通道可以在规则通道转换时,强行插入转换,相当于一个“中断通道”吧。当有注入通道需要转换时,规则通道的转换会停止,优先执行注入通道的转换,当注入通道的转换执行完毕后,再回到之前规则通道进行转换。

    调用ADC的三种方式

    轮询

    就是用while不断读取ADC的数据,这会严重消耗CPU。

    中断

    触发源可以有外部中断、定时器,还可以自己直接配置寄存器的标志位(寄存器CR2的ADON位,1开始0停止)。

    DMA

    由DMA进行数据搬运,可以节省CPU资源,直接从ADC的寄存器传到对应的外设。(不知道能否存进变量,理论上可以,毕竟顶层代码的编写就是把数据存进了自己设置的数组缓存中,大概是这里讲的CPU是指ALU那一小块运算器,片内存储也算是外设吧)

    关于转换周期更深入的理论学习细看

    https://blog.csdn.net/qq_43743762/article/details/100067558

    CubeMX配置

    ADCs_Common_Settings:

    Mode:

    Independent mod 独立 ADC 模式,当使用一个 ADC 时是独立模式,使用两个ADC时是双模式,在双模式下还有很多细分模式可选,具体配置 ADC_CR1:DUALMOD 位。

    ADC_Settings:

    Data Alignment:

    Right alignment 转换结果数据右对齐,一般我们选择右对齐模式。

    Left alignment 转换结果数据左对齐。

    Scan Conversion Mode:

    Disabled 禁止扫描模式。如果是单通道 AD 转换使用 DISABLE。

    Enabled 开启扫描模式。如果是多通道 AD 转换使用 ENABLE。

    Continuous Conversion Mode:

    Disabled 单次转换。转换一次后停止需要手动控制才重新启动转换。

    Enabled 自动连续转换。

    DiscontinuousConvMode:

    Disabled
    禁止间断模式。这个在需要考虑功耗问题的产品中很有必要,也就是在某个事件触发下,开启转换。

    Enabled 开启间断模式。

    ADC_Regular_ConversionMode:

    Enable Regular Conversions 是否使能规则转换。

    Number Of Conversion ADC转换通道数目,有几个写几个就行。

    External Trigger Conversion Source
    外部触发选择。这个有多个选择,一般采用软件触发方式。

    Rank:

    Channel ADC转换通道

    Sampling Time 采样周期选择,采样周期越短,ADC
    转换数据输出周期就越短但数据精度也越低,采样周期越长,ADC
    转换数据输出周期就越长同时数据精度越高。

    ADC_Injected_ConversionMode:

    Enable Injected Conversions
    是否使能注入转换。注入通道只有在规则通道存在时才会出现。

    WatchDog:

    Enable Analog WatchDog Mode 是否使能模拟看门狗中断。当被 ADC
    转换的模拟电压低于低阈值或者高于高阈值时,就会产生中断。据说电压过高会导致芯片烧?

    UART

    有三种模式:
    堵塞轮询;中断;DMA。
    日后细究,常用的还是中断和DMA

    堵塞轮询

    中断(相比前者无需timeout)

  • 接收中断函数
    三个参数,句柄、缓存区指针和接收数据长度。
  • /**
      * @brief  Receives an amount of data in non blocking mode.
      * @note   When UART parity is not enabled (PCE = 0), and Word Length is configured to 9 bits (M1-M0 = 01),
      *         the received data is handled as a set of u16. In this case, Size must indicate the number
      *         of u16 available through pData.
      * @param  huart Pointer to a UART_HandleTypeDef structure that contains
      *               the configuration information for the specified UART module.
      * @param  pData Pointer to data buffer (u8 or u16 data elements).
      * @param  Size  Amount of data elements (u8 or u16) to be received.
      * @retval HAL status
      */
    HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
    
  • 中断回调函数
    可以看注释描述“Rx Transfer completed callbacks”,接收完成后才进入这个函数,可以用于做一些数据处理,但还是要尽可能快。
  • /**
      * @brief  Rx Transfer completed callbacks.
      * @param  huart  Pointer to a UART_HandleTypeDef structure that contains
      *                the configuration information for the specified UART module.
      * @retval None
      */
    __weak void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
    {
      /* Prevent unused argument(s) compilation warning */
      UNUSED(huart);
      /* NOTE: This function should not be modified, when the callback is needed,
               the HAL_UART_RxCpltCallback could be implemented in the user file
       */
    }
    

    SPI

    基础概述

    这是个全双工通信,需要同步时钟。

    通信形式可以为一主一从或者一主多从(然后通过片选信号实现选择从机)。

    尽力而为的通信协议,只负责通信不负责成功,如果需要校验可以加入循环冗余码通过软件检测。

    发送速度:没有限制,短板效应。

    SPI工作原理

    本质上是主从机的两个寄存器连接构成了一个循环移位寄存器,从而实现了数据的交换,个人觉得SPI的神奇之处就是很巧妙地同时做了两件事。当然,这只是底层的寄存器实现了交换。但是在读取数据是还是分了在时钟的两个沿开始的。后面会展开嗦嗦。

    由于他的这个工作原理,这就意味着如果主机要只收不发是不行的,要实现这个事情的方法就是发送空数据,不过hal库里面已经有封装好拆开收发的函数了,同理只发不收就是不管收到的数据或者把MISO断了,让从机的缓存器高位溢出就好。

    工作模式

    四种发送-采样模式

    取决于CPHA(即Clock Phase,时钟相位)和CPOL(即Clock
    Polarity,时钟极性),四种(发送-采样)组合,四种触发。

    CPOL即Clock
    Polarity,时钟极性,通信的整个过程分为空闲时刻和通信时刻,如果SCLK在数据发送之前和之后的空闲状态是高电平,那么就是CPOL=1;如果空闲状态SCLK是低电平,那么就是
    CPOL=0。

    CPHA即Clock Phase,时钟相位。

    因为SPI是同步通信,同步通信的一个特点就是所有数据的变化和采样都是伴随着时钟沿进行的,所以收发数据就要根据时钟沿触发,而收发需要时间,所以收发触发应该在两个不同的沿。举个栗子,如果主机在上升沿发数据,从机则应在下降沿采样数据。

    CPHA:等于1则在时钟的第一个沿发数据(即从空闲电平到有效电平那个沿)。反之,为0就是第二个沿。

    CPOL:1则高为空闲,0则低为空闲。

    栗子来了:

    选择什么模式一般是主机去配合从机,因为很多从机做出来就规定好了工作模式了,包括能接收的最大速度和每帧数据的大小。

    基础配置

    数据帧有8和16选择,CR1寄存器处配置,主要还得看从机行不行。

    位发送顺序(MSB:二进制首位)低位(LSB:二进制末位)

    杂七杂八

    关于换行和回车

    串口输出过程中遇到的问题:不换行。

    查了下资料:

    换行符\n,0x0a,下一行

    回车符\r,0x0d,回到本行开头

    问题没完全解决,十六进制显示时用\n可以实现,估计是软件问题。

    ———————————————————————半小时后分割线——————————————————————————-

    最后发现\r\n才能实现换行并回车。

    原因如下:

    是否换行取决于串口助手按哪种规则来解析回车换行。

    DOS和Windows:需要\r\n才解析为有效的回车换行,否则只有回行首或只有换行。

    Unix和Mac OS X:将\n解析为有效的回车换行。

    Macintosh/OS 9:将\r解析为有效的回车换行。

    数字后面加个U

    表示无符号整形。同理,加个L表示长整型。

    封装代码

    .h文件

    基本格式

    #ifndef _xxx_H_

    #define _xxx_H_

    /*your code,such as “include” and declare variable,function*/

    #endif

    如果要c和c++混用,就要在#define和#endif之间用这段格式(相当于上面的代码行替换成下面这个格式):

    #ifdef __cplusplus

    extern “C” {

    #endif

    /*your code */

    #ifdef __cplusplus

    }

    #endif

    变量设置

    一般应该要加extern前缀,这样在多次include的时候才不会导致多次重复定义。(对应文件的static变量被extern后再别的文件中include这个头文件时好像相当于没有定义这个变量,会报错未定义)

    .c文件

    最后一行要是全新的一行,行里不能有空格,否则会警告。last line of files ends
    withou a newline

    各类型数据的格式符

    最近经常用USB的虚拟串口查看单片机运行中的各个数据,但是时不时就出bug,主要是因为发送数据时用的打印格式符跟变量类型不符导致出各种奇奇怪怪的问题。这里整理一下各个格式符:

    整型的不同长度格式符:

    d,lx,ld,,lu,这几个都是输出32位的

    hd,hx,hu,这几个都是输出16位数据的,

    hhd,hhx,hhu,这几个都是输出8位的,

    lld,ll,llu,llx,这几个都是输出64位的,

    printf( "%llu ",…)

    %llu 是64位无符号

    %llx才是64位16进制数

    %d 有符号32位整数

    %u 无符号32位整数

    %lld 有符号64位整数

    %llx 有符号64位16进制整数

    %#llx 带0x的64位16进制格式输出


    浮点数格式符:

    %f(普通方式)32位浮点数,对应float(double也可以用)

    %lf,对应double

    %e或%E(指数方式)

    %g或%G(自动选择)

    步进电机

    步进电机的转子是磁铁,外壳是线圈,和直流电机相反。控制步进电机的步进本质上就是控制电磁场,逐步改变磁场从而改变转子的平衡位置。

    以二相电机为例子:(这里正负为一组线圈,是连通的,连通的线没画出来)


    四拍的如上图,无论多慢也能有充足的动力,不会像直流电机的PWM那样为了慢占空比过小导致动力不足(我凭经验猜的,没求证,只是以前开发舵机的时候有试过占空比太小转动很吃力。但是!!!经过实验,步进电机不能太快,道理很简单,转子反应跟不上磁场变化,就转不起来了)。其实上面还不是最强的动力,如果两个线圈都通电,那么转子的四步应该四种45度的状态,,理论上磁力能增加到原来的根号2倍。

    延续一下上面45度的想法,还可以把电机状态发展成八拍的,如下图:

    用点阵显示文字图案

    比如打印机、LCD屏幕,他们输出的文字图案都是通过点阵实现的,一般都是多个像素行构成的字和图案,文字的字模不一定是按文字的形状排的,比如一个16*16的字,他需要16*16个像素点,但是他的字模可以是一个有32字节的一维矩阵(1*256像素),打印输出的时候就需要重新排列为文字显示的格式,每次取2字节作为1行,个字打印16行,这样才能得到一个字。一个小算法:

    /*循坏1:打印行数等于像素点数时刚好在纸上显示为1行Word_Pixel*Word_Pixel的字*/
    /*循坏2:字体缓存矩阵是每个字1行的,取第j个字的第k行像素点存入trans_buf*/
    	/*循环3:每行像素点有(pixel/8)个字节,每次取1字节放入打印缓存中,直到放完这行像素点*/
    for(k = 0;k < Word_Pixel;k++)
    		{
    			for(j = 0;j<Word_Num_Max;j++)
    			{
    				for(i = 0;i < 2;i++)
    				{
    					Printer_trans_buf[j*2+i] = word_buf[j][k*2+i];
                        //p_t_b为每次输出一行像素点所用的缓存,w_b为存放字模的缓存。
    				}
                 }
                 Heat_on();//每次输出一行像素点
              }
    
    

    如果需要接收外部编码然后从字库取字的话,可以再做一个缓存数组,然后把宏观态的字在这个数组中排列(不关心像素点,按字排即可,通过排列编码实现),然后打印时逐行像素打印这个矩阵的行即可。如下代码:

    /*外部输入字数组转换打印字数组(宏观的,不考虑像素)*/
    /*后面打印像素在这按行取字*/
    	uint8_t word_buf_row,word_buf_column;//word_buf的行列数
      uint8_t WordBuf[16][24]={0};//16行,48/2=24个字符。
    	void fsbuf_trans_wordbuf(uint16_t fs_rev_len)
    	{
    		static uint16_t fs_revBuf_num,num,u;
    		fs_revBuf_num = fs_rev_len;
    		num = 0;
    		while(fs_rev_data_len)
    		{
    			for(word_buf_row = 0; ;word_buf_row ++)
    		  {
    			  for(word_buf_column=0;word_buf_column<24;word_buf_column++)
    			  {
    			    if(num > fs_revBuf_num) 
    					{
    						return ;//跳出多重循环
    					}
    					/*判断回车换行*/
    					if(0x0d == fs_rev_buf[num]  && \
    						0x0a == fs_rev_buf[num+1] )
    					{
    						/*判断回车换行不在行首,且本行不是只多了1个空格则填满整行*/
    						if((word_buf_column>1)||(WordBuf[word_buf_row][0] != 0x20) )
    						{
    							for(u = word_buf_column;u < (24);u++)
    						  {
    							 WordBuf[word_buf_row][u] = 0;
    							}
    							
    							word_buf_row = word_buf_row + 1;//换行
    						  word_buf_column=0;//回车
    						}
    						num = num + 2;//跳过回车和换行
    					}
    					WordBuf[word_buf_row][word_buf_column] = fs_rev_buf[num];
    					num ++;
    			  }
    		  }
    		}
    	}
    
    
    
    /*将收到的字符通过ASCII库转成可输出的像素行*/
    	void Ascii_print(void)
    	{
    		extern uint8_t word_buf_row,word_buf_column;
    		extern uint8_t WordBuf[16][24];
    		static uint16_t m,n,h;//
    		static uint8_t groups ;//1组像素即一行字
    		static uint8_t A_Printer_trans_buf[48]={0};
    
    		if(word_buf_row||word_buf_column)//判断存储数据是否为0
    		{
    			for(groups = 0;groups < word_buf_row;groups++)//1行字
    			{
    				for(n = 0;n < 24; n++)//24行像素
    				{
    					for(h = 0;h < 24; h++)//24个字
    					{
    						for(m = 0;m < 2; m++)//第h个字的第n行像素点放进传输缓存的第2h位置的后两字节
    						{
    							A_Printer_trans_buf[h*2+m] =  \
    							Ascii_Lib_24[(uint16_t)WordBuf[groups][h]*48+n*2+m];//第2h位置的后两字节中的第m个字节存入1字节像素
    						}
    					}
    					Printer_data_out(A_Printer_trans_buf);
    					Printer_heat_all(1);//调试时置0,打印时为1
    					delay_us(2000);
    					Printer_heat_all(0);
    					delay_us(2000);
    				}
    			}
    			
    			fs_rev_data_len = 0;
    			fs_rev_cnt = 0;
    		}	
    	}
    

    关于hal库中的timeout参数

    这个参数多见于(普通,即不中断不DMA)收发数据,他是个超时参数,但是他不是通过延时实现判断是否超时的,它使用的是get_systick函数然后对比前后两次,如果差值大于超时值就是超时了。

    用keil5仿真调试查看输出波形

    选择simulator模式,CPU
    DLL这四个要改成这样(parame按理说是对照右边,但是这里右边没有)

    仿真后找到逻辑分析仪,选择信号,可以这么输入查看引脚,display
    type那里要选bit。也可以在view->symbols里找里面的寄存器查看。

    #ifndef、#define、#undef

    组合1:如果未定义则定义,用于防止重定义
    #ifndef XXX
    #define XXX
    #endif
    组合2:取消定义再重新定义
    #undef XXX
    #define XXX

    物联沃分享整理
    物联沃-IOTWORD物联网 » 【学习笔记】STM32hal库开发入门笔记

    发表评论