龙芯1c库是把龙芯1c的常用外设的常用功能封装为一个库,类似于STM32库。Git地址:https://gitee.com/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的精度。
感谢耐心看完,谢谢!