发帖数

50

原创数

50

关注者

12

阅读数

8965

点赞数

4

蔡琰

  • C语言与汇编混合工程

    大家好!我是张飞实战电子蔡琰老师,今天给大家分享C语言与汇编混合工程。

    除了汇编语言工程和C语言工程,许多嵌入式工程既包含C语言又包含汇编语言。由于Keil MDK的默认启动代码是用汇编编写的,但是又是独立的一个启动代码是汇编实现,一般其他程序都是C语言实现。

    我们一起来看下混合语言工程中,就比如在汇编程序中调用C函数,或者在C语言代码中调用汇编函数需要注意什么,稍不留意结果就可能是无法预测的。比如程序在一种版本的编译器下可以正常工作,而在另外的版本下,或者更换编译器后,工程可能会由于寄存器的使用冲突而停止工作。

    1、在汇编中调用C函数

    当在汇编文件中调用C函数的时候,需要注意一下方面:

    ①寄存器R0R3R12以及LR可能会被更改,如果这些寄存器中的数据之后还要使用,就需要将它们保存到栈上。

    SP的值应该是双字对齐的

    ③需要确保输入参数存储在正确的寄存器中(比如简单例子,使用R0~R3

    ④返回值(假定为32位或更小)一般存在R0

    举个例子:如果有一个将四个值相加的函数:

    int my_add(int x1,int x2,int x3,int x4)

    {
    return (x1+x2+x3+x4);

    }

    Keil MDK中,可以使用以下的代码在汇编中调用C函数:

    MOVS  R0,#1    ;第一个参数(x1

    MOVS  R1,#2    ;第二个参数(x2

    MOVS  R2,#3    ;第三个参数(x3

    MOVS  R3,#4    ;第四个参数(x4

    IMPORT my_add

    BL     my_add   ;调用“my_add”函数,结果保存在R0

    如果汇编代码是按照C文件中的嵌入式汇编编写的,应该使用_CPP关键字代替IMPORT关键字来引入地址符号。

    _CPP的用法如下:

    上例程中:

    IMPORT my_add

    BL     my_add   ;调用“my_add”函数,结果保存在R0

    改为:

    BL    _CPP(my_add)  ;调用“my_add”函数,结果保存在R0

    Keil  MDK中,_CPP关键字用于访问CC++编译时的常量表达式,而对于其他工具链,情况可能就有所不同了。

    2、C代码中调用汇编函数

    如果要从C代码中调用汇编函数,在实现汇编函数时,需要注意一下几点:

    ①若改变了寄存器R4R11里的任何数值,需要将原始数值保存到栈中,并且在返回到C代码以前恢复原始值。

    ②若要在汇编函数中调用另一个函数,需要将LR的值保存在栈中,并且利用它执行返回操作。

    ③函数返回值一般存在R0

    举个例子:如果一个实现4个数相加的汇编函数:

    EXPORT   my_add

    my_add FUNCTION

    ADDS  R0,R0,R1

    ADDS  R0,R0,R2

    ADDS  R0,R0,R3

    BX   LR    ;返回值在R0

    ENDFUNC

    C代码中,需要将函数声明为:

    extern  int my_add(int x1,int x2,int x3,int x4);

    int y;

    ……

    y= my_add(1,2,3,4);//调用my_add函数

    如果汇编代码需要访问C代码中的一些变量,也可以使用IMPORT关键字。

    大多数情况下,可能只需要一到两个简单的汇编函数,所以就想将这些汇编代码嵌入C代码的文件中。多数开发工具都有一种被称作内联汇编的特性,而ARM工具链则采用了另外一种特性“嵌入汇编”。

    通过嵌入汇编,我们可以在C文件中实现汇编函数。例如,将4个参数相加的函数可以如下写法:

    _asm int my_add(int x1,int x2,int x3,int x4)

    {

    ADDS  R0,R0,R1

    ADDS  R0,R0,R2

    ADDS  R0,R0,R3

    BX    LR  ;返回值在R0

    }

    可以在C代码中像普通C函数一样调用这个函数:

    y = my_add(1,2,3,4);

    嵌入汇编允许你在异常处理中定位栈帧,这也是嵌入汇编的一个优势。

     


    收藏 0 回复 0 浏览 71
  • BMS电池管理系统

    大家好!我是张飞实战电子蔡琰老师,今天给大家分享BMS电池管理系统


    随着新能源、可穿戴设备等的发展,BMS成为热议的话题,经久不衰,这篇文章我们就一起来学习一些关于BMS的知识。



    Q1.什么是BMS?

    BMS是对电池进行监控和管理的系统(Battery Management System),通过对电池的电压、电流、温度、剩余容量(SOC)、放电功率,报告电池劣化程度(SOH)、电池均衡管理、报警提醒等参数采集、计算进而控制电池放电过程,实现对电池的保护,提升电池综合性能的管理系统。它还根据电池的电压电流及温度用算法控制最大输出功率以获得最大续航,用算法控制充电机进行最佳电流的充电,它通过通信线与其他控制单元进行通信,比如我们手机或新能源汽车的快充、电量显示等都与BMS有着密切的关系。



    Q2.动力电池和非动力电池区别?

    提到电池,一般被分为动力电池和非动力电池,新能源汽车的动力来源一般主要是以动力电池为主。一般,动力电池实际上就是为交通运输工具提供动力来源的一种电源。它与普通电池的主要区别为动力电池相比于普通电池,其放电功率大。它可以在很短的时间内将电池的电放完。动力电池一般容量要小于非动力电池。



    Q3.什么是充放电倍率?

    一般电池后面的xxC它表示了电池的放电能力,C:用来表示电池充放电电流大小的比率,即倍率。充放电倍率=充放电电流/额定容量,例如1000mAh的电池,1C放电,就是1A放电,那么1个小时就可以放完,2C放电,就是2A放电,半个小时就可以放完。当然不同材质的电池,放电能力也是不一样的。



    Q4.什么是均衡控制?

    生产制造和使用过程的差异性,造成了动力电池单体天然就存在着不一致性。不一致性主要表现在单体容量、内阻、自放电率、充放电效率等方面。单体的不一致,传导至动力电池包,必然的带来了动力电池包容量的损失,进而造成寿命的下降。


    根据木桶短板效应,充电和放电时都是性能最差的单体先达到截止条件,其他还有一部分能力并未释放出来,这样就造成了浪费。


    电池单体的不一致,会随着时间的推移,在温度以及振动条件等随机因素的影响下进一步恶化,趋势无法逆转,但可以干预,降低它的恶化速率。方法之一就是通过电池管理系统对电芯实施均衡。


    均衡包括主动均衡和被动均衡,被动均衡,运用电阻器,将高电压或者高荷电量电芯的能量消耗掉,以达到减小不同电芯之间差距的目的,是一种能量的消耗。主动均衡,运用储能器件等,将荷载较多能量的电芯部分能量转移到能量较少的电芯上去,是能量的转移。


    但是均衡也存在一定的局限性,被动均衡,电流无法完全按照实际需求去做,因为通过电阻消耗的能量,转化成热量,对电池管理系统以及电池包都会产生不良影响;主动均衡,需要配置相应电路和储能器件,体积大,成本上升,这两个条件一起决定了主动均衡不容易推广应用。电池包的每个充电放电过程,都伴随着一部分电池局部的附加充放过程,无形中增加了电池的循环次数。



    Q5.一般BMS管理系统是什么样的?

    在一些系统中对成本的要求比较高,比如手扶电动两轮车,在这个系统中BMS电池管理系统设计也相对简单一些,仅由一颗专用的管理芯片即可实现电池管理,当然这个管理是纯硬件的,没有加入软件,实现一些基本的功能:正常充放电、过充过放、温度保护、平衡等。


    在复杂的管理系统中,从系统层次来进行架构管理,会引入一些控制单元(如单片机)做一些控制算法,它不仅包含硬件,还包含底层软件、应用层软件等。

    image.png

    如上图(来源于网络),分为主控,从控,电压电流采集控制、仪表显示等功能,非常复杂。单单这一个系统里面用到的单片机数量是十分可观的。


    上面我们简单对BMS一些问题进行了了解,作为控制工程师,首先学好单片机是非常重要的,还等什么,赶紧行动起来吧!


    收藏 0 回复 0 浏览 114
  • 单片机的时钟源

    大家好!我是张飞实战电子蔡琰老师,今天给大家分享单片机的时钟源


    在单片机中,比如我们常用的STM32是有五个时钟源的,分别是:HSI,LSI,HSE,LSE,PLL。


    这些时钟源都体现在时钟树,我们知道每个型号的单片机手册都有时钟树的展示,这样让我们能清楚知道应用到的外设是通过哪个时钟源分频或倍频得来的。我们以ST推出的STM32G030为例来看下:

    image.png

    1、HSI是高速内部时钟,RC振荡器,频率为16MHz。

    2、LSI是低速内部时钟,RC振荡器,频率为32KHz。

    3、HSE是高速外部时钟,可接晶体/陶瓷振荡器,或者接外部时钟源(Bypass模式),频率范围是4-48MHz。

    4、LSE是低速外部时钟,接频率为32.768KHz的石英或谐振器,或者使用旁路模式引入外部时钟源。

    5、PLL为锁相环倍频输出,其时钟输入源可选择为HSE、HSI。倍频可选择为1~8倍,但是其输出频率最大不得超过64MHz(注意单片机的最大频率)。


    我们看到LSI(32KHz)供独立看门狗IWDG使用,另外它还可以被选择为实时时钟RTC的时钟源。


    当然我们看到RTC的时钟源还可以选择LSE,或者是HSE的32分频。当然选择哪个作为最终的时钟源就是通过寄存器来配置了。

    这个LSI(低速内部时钟)可以用于除Shutdown和VBAT模式之外的所有模式。


    系统时钟是可以提供绝大部分工作的时钟源,是可以通过LSE、LSI、HSE、PLL、HSI分频获得。系统时钟可以分频给到总线去分到各个外设上,还可以直接提供给外设,比如ADC和I2S。系统时钟、AHB总线的最大时钟可以是64MHz(单片机支持最大频率)。


    HSI时钟可以衍生HSISYS,HSISYS这个可以从Stop0和Stop1模式唤醒后被选为时钟源,也可以当做备份时钟源。


    I2C、UART、LPUART在Stop模式下如果探测到了从Stop模式下唤醒的序列则能够自动使能HSI16时钟。(HSI16时钟在Stop模式下保持关闭的状态,除非探测到了外设唤醒序列)


    HSE的时钟安全系统(CSS),自动检测到HSE失效时切换到HSI16。


    LSE的时钟安全系统适用于除了Shutdown和VBAT模式之外的所有模式,在复位状态下仍然有效。


    LSE可用于RTC, U(S)ARTs, LPUART, LPTIMs.

    PLL可以给到三路输出,是可以获得的最大频率不一样,给到的外设不一样,PLLPCLK是最大频率可以做到122MHz,可以给到ADC或者I2S。PLLQCLK最大频率是112MHz,是应用到定时器的。PLLRCLK最大频率是56MHz,主要是给到系统时钟。

    多个时钟源的好处是在选择时钟源的时候具有高度的灵活性,可满足功耗和精度的要求。


    许多独立的外设时钟允许在不影响通信波特率的情况下调整功耗,并在低功耗模式下保持一些外设有效。


    现在很多单片机都考虑到了低功耗,自然时钟也是其中很重要的部分了。


    对于应用外设时钟这块还是很重要的,需要会看时钟树,会对应用需要的时钟进行配置,这个就显得很重要了,比如想要高速时钟需要倍频,如果是低功耗需要分频,首先还是需要去看时钟的分布,才好再去应用的。




    收藏 0 回复 0 浏览 161
  • 单片机延时的两种实现方法

    大家好!我是张飞实战电子蔡琰老师,今天给大家分享单片机延时的两种实现方法


    单片机延时大家程序中都会用到,那么怎么实现呢?通常我们有软件实现,就是语句循环;或者硬件实现,通过定时器/计数器。


    对于精准而言,从实现语言方向看,底层语言有汇编和C语言,通常我们单片机程序用C语言比较多,但是相对而言汇编更精准。从实现方式来看,有软件方式和硬件方式,那么对于C语言延时的方法哪种更精准呢?自然是定时器计数器的方式了,为什么呢?一起来看看吧。


    1、软件延时-循环实现


    在很多时候,定时器会被用作其他用途,不方便再用作计数了,所以这个时候就只能用软件方法延时。软件方法延时就是循环语句来实现。


    通过使用带_NOP_();语句的函数实现,定义一系列不同的延时函数。我们都知道C语言最终通过编译生成汇编,所以一条C语言可能会反汇编成多条汇编语句,每条汇编指令都有指令周期,比如我们时钟是8Mhz的,那么一个指令周期就是125ns,那么NOP是一个空指令,占用一个指令周期。假如我们定义一个1ms的延时函数,那么函数里面全部用NOP指令也需要循环多次,那么循环语句也可能会有多条汇编实现,具体的根据编译器不同也不同,我们就很难精确的计算出实际的延时,那么计算大概也可以,然后最有效直接的方法就是通过示波器去测试然后再去调整循环数。不要太纠结计算数值,可以调试的。示波器测试方法更简单,在延时前后加一个引脚的输出反转信号就可以了。


    到这里大家是不是觉得其实用汇编写延时函数就能做到精确了呢?相比较C语言,的确是汇编可以做到很精确的数值,因为我们可以确定每条指令的指令周期是多少,根据延时函数用到的所有指令都可以计算出来,最终计算得出比较精准的循环数值。


    2、硬件延时-定时器/计数器实现


    当然在大多情况下,我们还是会选择定时器来做延时处理,首先我们可以通过时钟配置定时器工作,获得精准计数,具体精准程度要看给定时器用的时钟了,内部时钟或者外部晶振的精度。可以实现极短时间的精确延时。


    在实际应用中,定时常采用中断方式,通过对定时器的配置,获得中断方式和定时时间,然后通过判断计数值获得想要的延时效果,用这种方法从程序的执行效率和稳定性方面考虑都是最佳的方案。大部分项目主循环需要处理很多事情,如果在主循环中用软件延时方法难免需要等待过程,尤其是长延时的时候不能处理其他,如果好多外设在工作,会造成通信不上或者响应不及时。


    总结:大部分程序中我们可以写个小的软件延时,必须等待的小延时可以用软件的实现,就可以省去对延时时间的判断了,等待就可以了。但是需要测试延时时间是否准确,或者可以直接用汇编写延时函数。硬件延时也有必要实现,根据实际需求去应用。


    大部分延时应用过程中还是会有些误差的,这个是在多个小的误差基础上叠加的,是可以接受的。


    收藏 0 回复 0 浏览 265
  • hex文件在单片机中是如何工作起来的?

    大家好!我是张飞实战电子蔡琰老师,今天给大家分享单片机hex文件。

    通常我们写的单片机程序放在哪里了?ROM和RAM有什么区别?有没有关系呢?程序又是怎么工作起来的?其实通常这些问题对于单片机初级工程师来说可能都是大大的问号,好多人可能会让单片机工作起来,比如有例程,然后根据烧录工具的流程走一遍就觉得会用单片机了,好多人会想单片机如此简单还有什么可学的。


    太多人是不学底层的工作原理,也不想。觉得那么多库函数和例程,用起来就可以了。可是自己真的懂吗?碰到问题你会解决吗?所以还是会了别人不会的才是有更多选择的资本。


    RAM:随机存储器,掉电数据会丢失,也就是会存储一些临时数据。

    ROM:只读存储器,掉电数据不会丢失,主要存储程序数据以及常量数据或需要存储的变量数据


    概念有了,到底是怎么工作起来的?

    单片机运行时需要调用程序或者函数时就需要读取ROM,然后在RAM中执行这些程序或者函数的功能,所产生的临时数据也都存在RAM,掉电后临时数据就会丢失了。


    程序经过编译、汇编、链接后生成HEX文件,然后再通过烧录器将HEX文件烧录到ROM中。


    那么ROM是只读存储器,CPU只能从里面读数据,而不能往里面写数据,掉电后数据依然保存在存储器中。RAM是随机存储器,CPU可以从里面读取数据也可以写数据,掉电不保存。


    那么这里就涉及到了单片机启动的过程了,就像我们电脑,手机这些设备开机都需要一个启动过程,就相当于初始化的过程,并不是直接跑到主程序的,单片机也是一样,这个就是启动流程,之后才会跑到主程序运行的。


    启动流程我们拿ST的一款来说一下,其他厂家的也是大同小异的。通常,当处理器从复位启动时,它首先会访问 0 地址的向量表,复位时处理器首先读取向量表的前 2 个字,第一个字为堆栈指针 MSP 初始值;第二个字为复位向量,它表示程序执行的起始地址。当读取到复位地址之后,会自动跳到复位向量处开始执行程序。简单的说上电的时候就是要从地址0x0000 0000处开始的,那么从地址0处开始存储了一个向量表,就是我们说的中断向量表。换句话来讲,我们写的代码是在FLASH区域的,那么复位向量地址也是在FLASH区域,那么就相当于程序跳到了FLASH存储区开始执行代码。


    那么RAM上电数据不是人为写入的,就是CPU写入的,就是上电启动后CPU开始从ROM中包含的所有程序内容第一行代码处开始执行指令的。然后为后面整个程序运行做准备,需要对RAM进行初始化。


    首先是对变量分配地址空间,有已赋初值的全局变量,就需要将初值从ROM中拷贝到RAM中。


    没有赋初值,就是随机值。

    其次设置堆栈的地址,一般我们只用到栈空间,也就是中断处理时的“保护现场”和“恢复现场”的作用。


    最后分配代码中的数据段,也是从ROM中拷贝到RAM中来分配起始地址。


    启动之后完成这些准备工作就是根据不同的指令一条一条的执行了,发生中断的时候会根据中断向量表找到中断服务函数去执行中断,整个程序就这样的过程跑起来了。


    收藏 0 回复 0 浏览 221
×
蔡琰