【龙芯1c库】封装软件延时接口和使用示例

来源:互联网 发布:淘宝退款被拒绝怎么办 编辑:程序博客网 时间:2024/06/16 15:34

龙芯1c库是把龙芯1c的常用外设的常用功能封装为一个库,类似于STM32库。Git地址:http://git.oschina.NET/caogos/OpenLoongsonLib1c

程序中难免会用到延时函数,一般通过执行n个nop指令实现延时。为此封装了delay_us(i), delay_ms(i), delay_s(i)三个函数,分别延时ius, ims, is。并测试了几个函数的延时精度,除了延时时间为几微秒时,精度稍微差一些之外,其它延时时间长度,误差都尽量控制在了一两个单位之内。

本文先讲解封装的延时接口函数如何使用,再展示测试的效果,再分析延时函数的源码,最后再尝试着优化并给出优化后的代码和测试结果。

龙芯1c库中软件延时接口使用示例

软件延时接口简介

共提供三个接口,微秒,毫秒,秒三个级别的延时各一个,如下

/* * 延时指定时间,单位ms * @j 延时时间,单位ms */void delay_ms(int j);/* * 延时指定时间,单位us * @n 延时时间,单位us */void delay_us(int n);/* * 延时指定时间,单位s * @i 延时时间,单位s */void delay_s(int i);

比如,延时50us,调用语句为delay_us(50);

延时50ms,调用语句为delay_ms(50);

延时1s,调用语句为delay_s(1);

为每个接口设计了一个测试用例,通过延时指定时间将gpio拉低拉高,用示波器观察波形的方式来测试,微秒和毫秒的用例里面依次产生占空比为0.5,周期为2个单位,10个单位,100个单位和500个单位的pwm波形。秒的测试用例里只产生周期2s和10s的pwm。

测试delay_ms()

测试代码

// 测试延时函数delay_1ms()void test_delay_1ms(void){    unsigned int gpio = 6;    int time = 0;    gpio_init(gpio, gpio_mode_output);    gpio_set(gpio, gpio_level_high);    // 产生不同宽度的高低电平,用示波器观察高低电平宽度是否正确    while (1)    {        // 2ms        time = 2/2;        delay_ms(time);        gpio_set(gpio, gpio_level_low);        delay_ms(time);        gpio_set(gpio, gpio_level_high);        // 10ms        time = 10/2;        delay_ms(time);        gpio_set(gpio, gpio_level_low);        delay_ms(time);        gpio_set(gpio, gpio_level_high);        // 100ms        time = 100/2;        delay_ms(time);        gpio_set(gpio, gpio_level_low);        delay_ms(time);        gpio_set(gpio, gpio_level_high);        // 500ms        time = 500/2;        delay_ms(time);        gpio_set(gpio, gpio_level_low);        delay_ms(time);        gpio_set(gpio, gpio_level_high);            }}

测试结果

每格200ms时,可以看到好几个完整的波形

周期为500ms的,实际测量结果为497ms

周期为100ms的,实际测量结果为98.9ms

周期为10ms的,实际测量结果为9.91ms

最后再来看看,周期为2ms的,实际测量结果为1.98ms。

如果觉得这个精度不够,还可以微调,给代码中k_max一个补偿值(代码在后面)。

测试delay_us()

测试代码

// 测试延时函数delay_1us()void test_delay_1us(void){    unsigned int gpio = 6;    int time;    gpio_init(gpio, gpio_mode_output);    gpio_set(gpio, gpio_level_high);    // 产生不同宽度的高低电平,用示波器观察高低电平宽度是否正确    while (1)    {        // 2us        time = 2/2;        delay_us(time);        gpio_set(gpio, gpio_level_low);        delay_us(time);        gpio_set(gpio, gpio_level_high);        // 10us        time = 10/2;        delay_us(time);        gpio_set(gpio, gpio_level_low);        delay_us(time);        gpio_set(gpio, gpio_level_high);        // 50us        time = 50/2;        delay_us(time);        gpio_set(gpio, gpio_level_low);        delay_us(time);        gpio_set(gpio, gpio_level_high);        // 100us        time = 100/2;        delay_us(time);        gpio_set(gpio, gpio_level_low);        delay_us(time);        gpio_set(gpio, gpio_level_high);        // 500us        time = 500/2;        delay_us(time);        gpio_set(gpio, gpio_level_low);        delay_us(time);        gpio_set(gpio, gpio_level_high);    }}


这里多增加了一个延时时间50us。因为源码中将1到1000us分为三段(1-10,10-100,100-1000),分别优化,以尽量提高延时精度,不论延时时间长度是多少。

测试结果


每格为200us时,看到的整个波形

周期为500us的波形,实际测量值为501us。

周期为100us的波形,实际测量值为98.7us
程序不变,按复位键复位后,相邻两次测量可能有1us左右的偏差。

周期为50us的波形,实际测量为51.9us

周期为10us的波形,实际测量值为10.4us

周期为2us的波形,实际测量值为5.46us。
从这个测试结果看来,当延时时间只有几微秒时,误差比较大。当然应该可以再继续优化达到更好效果,我这里就不优化,感兴趣的可以自己优化试试。

测试delay_s()

测试代码

// 测试延时函数delay_1s()void test_delay_1s(void){    unsigned int gpio = 6;    int time;    gpio_init(gpio, gpio_mode_output);    gpio_set(gpio, gpio_level_high);    while (1)    {        // 2s        time = 2/2;        delay_s(time);        gpio_set(gpio, gpio_level_low);        delay_s(time);        gpio_set(gpio, gpio_level_high);        // 10s        time = 10/2;        delay_s(time);        gpio_set(gpio, gpio_level_low);        delay_s(time);        gpio_set(gpio, gpio_level_high);    }}


测试结果

周期太长,示波器都不能完整显示一个周期的。

周期10s的波形,周期太长,只能看到一部分,这里测量了一下高电平部分的宽度。实际测量结果为4.99s

最后来看看周期为2s的实际结果,实际测量值为1.99s。

封装延时函数接口

要点

处理器有nop汇编指令,就是执行空操作,什么也不做,就占用一点时间而已。既然是执行n个nop指令,理论上可以通过控制nop指令执行的次数n实现“精确”延时,基于这点理论基础,本文尽可能的提高延时精度。
如果编译选项开启了-O2优化选项的话,不论是while还是for循环执行空操作很容易被编译器优化掉。避免被编译器优化掉,在for循环中使用__asm__ ("nop")代替“;”执行空操作。
当延时时间很短时,比如几us,可能需要考虑GPIO反转速度,除了for循环之外函数内其它指令占用的时间等等。
有必要的话可以使用命令“mipsel-linux-objdump -S OpenLoongsonLib1c > objdump.txt”反汇编,可以查看到更多细节。

源码清单

delay_ms()

/* * 延时指定时间,单位ms * @j 延时时间,单位ms */void delay_ms(int j){    int k_max = clk_get_cpu_rate()/1000/3;  // 除以1000表示ms,除以3为测试所得的经验(可以理解为最内层循环执行一次需要的时钟个数)    int k = k_max;    for ( ; j > 0; j--)    {        for (k = k_max; k > 0; k--)        {            __asm__ ("nop");        // 注意,这里必须用内联汇编,否则会被优化掉        }    }    return ;}

代码很简单,应该能看懂。最里层for循环延时1ms,外面一层for循环根据入参j的值,控制执行多少个延时1ms,实现延时jms。

重点分析一下最里层的for循环,总共执行k_max个nop指令,变量k_max值的大小指定决定延时时间长度。为了便于移植,这里选择读取cpu的频率,如果没有流水线的话,通常一个cpu时钟执行一个nop指令,但龙芯1c是有流水线的,再加上最里层的for循环出来nop指令之外,至少还有判断K>0和k--。所以最里层的for循环执行一次需要的时间最好是通过反汇编来看,这里选择了通过测量来获得一个经验值,经过测量3个cpu时钟执行一次最里层的for循环,这也是k_max初始值里面需要cpu频率除以3的原因。
把cpu频率除以1000就是1ms内cpu时钟个数,再除以3(一个最里层for循环执行需要3个cpu时钟)就是for循环执行次数。
延时1ms,外层for循环执行一次,延时n毫秒,外层for循环执行n次。外层for循环这几条汇编指令需要的时间和1ms比起来,可以忽略不计,所以ms级延时精度应该可以比较高的。

delay_us()

/* * 延时指定时间,单位us * @n 延时时间,单位us */void delay_us(int n){    int count_1us = clk_get_cpu_rate() / 1000000 / 3;   // 延时1us的循环次数    int count_max;                                      // 延时n微秒的循环次数    int tmp;    // 根据延时长短微调(注意,这里是手动优化的,cpu频率改变了可能需要重新优化,此时cpu频率为252Mhz)    if (10 >= n)                // <=10us    {        count_1us -= 35;    }    else if (100 >= n)          // <= 100us    {        count_1us -= 6;    }    else                        // > 100us    {        count_1us -= 1;    }    count_max = n * count_1us;    // 延时    for (tmp = count_max; tmp > 0; tmp--)    {        __asm__ ("nop");        // 注意,这里必须用内联汇编,否则会被优化掉                }    return ;}

和延时1ms的代码类似,都是通过控制nop指令执行次数来控制延时时间。除了相同点之外,也有两个不同点

1,将1us到1000us整个区间,分为三段,目的是尽可能的提高每段的延时精度,减小误差,如果你问我为什么要分为三段,我只能说是测试经验告诉我,如果不分段,则很难将整个区间的精度都控制好,经验所得。
2,把两层for循环改为一层for循环。按照前面delay_1ms()的思路,这里也应该用两次for循环,通用经过实际测试,发现两层for循环很难将整个区间的误差控制好,因为延时时间越低,除最里层for循环之外的代码执行时间占整个延时时间的比重越大,也就是误差越大。为了尽量减少除最里层for循环的代码,这里选择了只用一层for循环的方法。
count_1us = clk_get_cpu_rate() / 1000000 / 3 = 252000000 / 1000000 / 3 = 84,即理论上当cpu频率为252Mhz时,延时1us需要执行for循环的次数为84次。

延时n微秒需要执行的次数count_max = n * count_1us = n * 84;源码中在计算循环次数之前,会微调一下变量count_1us的值,具体微调多少是根据测试效果来判断的,所以代码中微调的值都是经验值,如果cpu频率改变了,也应该相应改变,这可能会带来移植问题,但没办法。


delay_s()

/* * 延时指定时间,单位s * @i 延时时间,单位s */void delay_s(int i){    for ( ; i > 0; i--)    {        delay_ms(1000);    }    return ;}

考虑到通常需要延时1s的地方都不是需要非常高的精度,所以这里延时1s是通过延时1000ms实现的,即前面的ms级延时精度直接影响这里延时1s的精度。

感谢耐心看完,谢谢!