【Vivado HLS学习之CORDIC算法的实现】

【Vivado HLS学习之CORDIC算法】


先抛出一个问题:在FPGA上怎么实现三角函数sin,cos的计算?
以sin为例,在计算机上实现sin函数可以用泰勒展开来近似。
s i n ( x ) ≈ x − x 3 3 ! + x 5 5 ! − x 7 7 ! + x 9 9 ! + . . . sin(x) \approx x - \frac{x^3}{3!} + \frac{x^5}{5!} - \frac{x^7}{7!} + \frac{x^9}{9!} + ... sin(x)x3!x3+5!x57!x7+9!x9+...

这种方式看似简单高效,但不便于FPGA上实现;
一个常见的方法是查表法:预先计算好各个相位对应的sin值,将这些数值保存在存储器中,计算时直接查表即可。这种方式的计算精度取决于数据表的长度,想要获得更高的精度,就需要占用更多的存储空间,是一种典型的“空间换时间”思路。

还有其他更合适FPAG的方法,比如CORDIC算法。


接下来介绍CORDIC算法。

CORDIC(Coordinate Rotation Digital Computer)算法即坐标旋转数字计算方法,是J.D.Volder1于1959年首次提出,主要用于三角函数、双曲线、指数、对数的计算。该算法通过基本的加和移位运算代替乘法运算,使得矢量的旋转和定向的计算不再需要三角函数、乘法、开方、反三角、指数等函数。

–摘自百度百科
在这里插入图片描述

单位圆上有A和B两点,夹角为θ。A点绕单位圆逆时针旋转θ角就能得到B点,假设A点坐标为(x1, y1),B点坐标为(x2, y2)。坐标变换如下:
{ x 2 = x 1 cos ⁡ θ − y 1 sin ⁡ θ y 2 = x 1 sin ⁡ θ + y 1 cos ⁡ θ \left\{\begin{matrix} x_2 = x_1\cos\theta - y_1\sin\theta &\\ y_2 = x_1\sin\theta + y_1\cos\theta &\\ \end{matrix}\right. {x2=x1cosθy1sinθy2=x1sinθ+y1cosθ

将cosθ项提出,则得到
{ x 2 = ( x 1 − y 1 tan ⁡ θ ) cos ⁡ θ y 2 = ( x 1 tan ⁡ θ + y 1 ) cos ⁡ θ \left\{\begin{matrix} x_2 = (x_1 - y_1\tan\theta)\cos\theta &\\ y_2 = (x_1\tan\theta + y_1)\cos\theta &\\ \end{matrix}\right. {x2=(x1y1tanθ)cosθy2=(x1tanθ+y1)cosθ

(注意,如果A->B为顺时针旋转,则上述式中的加减号要互换)

CORDIC算法的思想就是,取一系列固定的角度θi,使得tanθi = 2^(-i),这样一来tan乘积项就可以通过位移操作来实现;而这些固定的θi可以通过查表得到

θ(弧度)tan(θ)
0.78539816339744831
0.46364760900080610.5
0.244978663126864140.25
0.124354994546761440.125
0.062418809995957350.0625
0.0312398334302682770.03125
0.0156237286204768310.015625
0.0078123410601011110.0078125
0.00390623013196697180.00390625
0.00195312251647881880.001953125
0.00097656218955931950.0009765625
0.00048828121119489830.00048828125

至于cosθ,这一项在多次迭代后其累乘结果收敛于一定值K,用下面这一小段代码计算N次迭代后K的值,当N取12时
K = 0.607252959138945

# 计算N次迭代后, 余弦累乘项K
from math import cos, atan, prod
K = lambda N : prod([cos(atan(2**-i)) for i in range(N)])

print(K(12))

所以在迭代时,一开始就从点(K, 0)坐标旋转,按照上面正切表从上到下逐一旋转对应角度,注意旋转方向要向着靠近目标角度的方向。因为一开始就考虑到了K,所以最后迭代完成,旋转到一点的位置,其横坐标x就是cos值,纵坐标y就是sin值。


接下来考虑如何在FPGA上实现算法。使用Q4.12格式的定点数,也就是4位整数,12位小数。因为使用到了Vivado HLS工具,这里给出C语言实现的CORDIC算法,迭代12次,使用定点数运算。

cordic_cos_sin.c

#include <ap_cint.h>

#define PIPELINE 		12
#define FIXED_PI		((uint16)0x3243) // 3.14159*2^12


const int16 ROT_TABLE[]={				//旋转角对照表(弧度rad*2^12)
	0x0c90, 0x076b, 0x03eb, 0x01fd,
	0x00ff, 0x007f, 0x003f, 0x001f,
	0x000f, 0x0007, 0x0003, 0x0001
};


// 输入phase定点数 为 弧度*2^12
// 范围0~2pi*2^12
// 输出定点cos, sin
void cordic_cos_sin(
	uint16* phase,
	int16 * cos,
	int16 * sin
){
#pragma HLS INTERFACE port=return
    int16 x0= 0x09b7; 	// 0.60725*2^12 起点坐标
    int16 y0= 0; 		//
    int16 x1= 0; 		// 旋转到下一点的坐标
    int16 y1= 0; 		//
    int16 theta; 		// 起始角度
	uint2 quad; 		// 确定象限

#pragma HLS PIPELINE
	if (*phase < FIXED_PI/2){
		theta = *phase;
		quad = 0;
	}else if (*phase < FIXED_PI){
		theta = FIXED_PI - *phase;
		quad = 1;
	}else if (*phase < 3*FIXED_PI/2){
		theta = *phase - FIXED_PI;
		quad = 2;
	}else if (*phase < 2*FIXED_PI){
		theta = 2*FIXED_PI - *phase;
		quad = 3;
	}else{
		theta = 0;
		quad = 0;
	}

	// cordic迭代
	for (int i = 0; i < PIPELINE; i ++){
#pragma HLS UNROLL
		if (theta < 0){
			theta += ROT_TABLE[i];
            x1 = x0 + (y0 >> i); // x1 = x0 + y0 * tan(...)
            y1 = y0 - (x0 >> i); // y1 = y0 - x0 * tan(...)
		}else{
			theta -= ROT_TABLE[i];
            x1 = x0 - (y0 >> i); // x1 = x0 - y0 * tan(...)
            y1 = (x0 >> i) + y0; // y1 = x0 * tan(...) + y1
		}
		x0 = x1; // 更新
		y0 = y1;
	}

    switch (quad)
    {
        case 0:
			*cos = x1;
			*sin = y1;
            break;
        case 1:
			*cos = -x1;
			*sin = y1;
            break;
        case 2:
			*cos = -x1;
			*sin = -y1;
            break;
        case 3:
			*cos = x1;
			*sin = -y1;
            break;
        default:
            break;
    }
}

再写一个main.c作为testbench

main.c

#include <stdio.h>
#include <math.h>
#include <ap_cint.h>


#define TEST_LEN 100 // 测试100个数

// 待测函数
extern void cordic_cos_sin(
	uint16 *phase,
	int16 * cos,
	int16 * sin
);

int16 float2Q4_12(float f){
	return (int16) (f * pow(2, 12));
}

float Q4_12_2float(int16 i){
	return (float)( i / pow(2, 12));
}

int main(int argc, char const *argv[]){
	printf("********************* cordic test begin *********************\r\n");

	for (int16 i = 0; i < TEST_LEN; i++){
		float phase_f = 2 * i * M_PI / (float)TEST_LEN;
		uint16 phase_fixed = float2Q4_12(phase_f);

		float cos_real = cosf(phase_f);
		float sin_real = sinf(phase_f);
		int16 cos_cordic_fixed = 0;
		int16 sin_cordic_fixed = 0;
		cordic_cos_sin(&phase_fixed, &cos_cordic_fixed , &sin_cordic_fixed );
		float cos_cordic = Q4_12_2float(cos_cordic_fixed);
		float sin_cordic = Q4_12_2float(sin_cordic_fixed);

		printf("cos(%0.4f): %0.4f / %0.4f, err: %0.4f\t",\
				phase_f, cos_real, cos_cordic, cos_real - cos_cordic);
		printf("sin(%0.4f): %0.4f / %0.4f, err: %0.4f\r\n",\
				phase_f, sin_real, sin_cordic, sin_real - sin_cordic);
	}
	return 0;
}

下面是运行C仿真打印的结果

********************* cordic test begin *********************
cos(0.0000): 1.0000 / 1.0002, err: -0.0002	sin(0.0000): 0.0000 / 0.0000, err: 0.0000
cos(0.0628): 0.9980 / 0.9985, err: -0.0005	sin(0.0628): 0.0628 / 0.0632, err: -0.0004
cos(0.1257): 0.9921 / 0.9922, err: -0.0001	sin(0.1257): 0.1253 / 0.1257, err: -0.0004
cos(0.1885): 0.9823 / 0.9827, err: -0.0004	sin(0.1885): 0.1874 / 0.1877, err: -0.0004
cos(0.2513): 0.9686 / 0.9688, err: -0.0002	sin(0.2513): 0.2487 / 0.2488, err: -0.0001
cos(0.3142): 0.9511 / 0.9509, err: 0.0001	sin(0.3142): 0.3090 / 0.3098, err: -0.0008
cos(0.3770): 0.9298 / 0.9299, err: -0.0002	sin(0.3770): 0.3681 / 0.3684, err: -0.0003
cos(0.4398): 0.9048 / 0.9045, err: 0.0003	sin(0.43a98): 0.4258 / 0.4265, err: -0.0007
cos(0.5027): 0.8763 / 0.8760, err: 0.0003	sin(0.5027): 0.4818 / 0.4827, err: -0.0009
cos(0.5655): 0.8443 / 0.8442, err: 0.0001	sin(0.5655): 0.5358 / 0.5359, err: -0.0001
cos(0.6283): 0.8090 / 0.8091, err: -0.0001	sin(0.6283): 0.5878 / 0.5872, err: 0.0006
(省略)

可以看出误差还是比较小的,在10^-4数量级

然后将模块打包成IP,导入Vivado测试:

block design
在这里插入图片描述

波形图
在这里插入图片描述

最后给出测试用的testbench.v

`timescale 1ns/100ps

module tb_cordic_sin_cos;
reg         clk;
reg         rst_n;
reg         input_en;
reg [15:0]  theta_rad_q4_12;

wire [15:0] cos_q4_12;
wire [15:0] sin_q4_12;
wire        output_en;

cordic_sin_cos u_0(
    .clk(clk),
    .rst_n(rst_n),
    .input_en(input_en),
    .theta_rad_q4_12(theta_rad_q4_12),
    .cos_q4_12(cos_q4_12),
    .sin_q4_12(sin_q4_12),
    .output_en(output_en)
);

localparam CLK_PERIOD = 10;
always #(CLK_PERIOD/2) clk=~clk;

initial begin
    $dumpfile("tb_cordic_sin_cos.vcd");
    $dumpvars(0, tb_cordic_sin_cos);
end

initial begin
    #1 rst_n<=1'bx;clk<=1'bx;
    theta_rad_q4_12 <= 0;
    input_en <= 0;
    #(CLK_PERIOD*3) rst_n<=1;
    #(CLK_PERIOD*3) rst_n<=0;clk<=0;
    repeat(5) @(posedge clk);
    rst_n<=1;
    input_en <= 1;
    
    for (integer i = 0; i < 25735; i = i + 10) begin
        @(negedge clk)
            theta_rad_q4_12 <= i;
    end
    $finish;
end

endmodule

感谢您的阅读,如有错误,欢迎指出

  • 0
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vivado HLS是一个高级综合工具,可以使用C/C++等高级语言编写代码,并将其转换为可综合的硬件描述语言。下面是一个简单的车牌识别算法的示例: 1. 读入图片并进行预处理 ```c++ void plate_recognition(unsigned char *input_image, unsigned char *output_image) { // 读入图片并进行预处理 cv::Mat in_img = cv::imread(input_image); cv::Mat gray_img; cv::cvtColor(in_img, gray_img, CV_BGR2GRAY); cv::Mat blur_img; cv::GaussianBlur(gray_img, blur_img, cv::Size(5, 5), 0); cv::Mat canny_img; cv::Canny(blur_img, canny_img, 50, 150, 3); cv::Mat dilate_img; cv::dilate(canny_img, dilate_img, cv::Mat(), cv::Point(-1, -1), 1, 1, 1); cv::Mat erode_img; cv::erode(dilate_img, erode_img, cv::Mat(), cv::Point(-1, -1), 1, 1, 1); // 将处理后的图片转换为灰度图像 int width = erode_img.cols; int height = erode_img.rows; unsigned char *gray_data = (unsigned char *)malloc(width * height); for (int row = 0; row < height; row++) { for (int col = 0; col < width; col++) { gray_data[row * width + col] = erode_img.at<unsigned char>(row, col); } } ``` 2. 进行车牌定位 ```c++ // 进行车牌定位 int plate_x = 0; int plate_y = 0; int plate_width = 0; int plate_height = 0; bool found_plate = false; for (int row = 0; row < height - 100; row++) { for (int col = 0; col < width - 200; col++) { int sum = 0; for (int i = 0; i < 100; i++) { for (int j = 0; j < 200; j++) { sum += gray_data[(row + i) * width + col + j]; } } if (sum > 100000 && sum < 150000) { plate_x = col; plate_y = row; plate_width = 200; plate_height = 100; found_plate = true; break; } } if (found_plate) { break; } } ``` 3. 对车牌进行字符分割和识别 ```c++ // 对车牌进行字符分割和识别 if (found_plate) { cv::Mat plate_img = in_img(cv::Rect(plate_x, plate_y, plate_width, plate_height)); cv::Mat gray_plate; cv::cvtColor(plate_img, gray_plate, CV_BGR2GRAY); cv::Mat thresh_plate; cv::adaptiveThreshold(gray_plate, thresh_plate, 255, cv::ADAPTIVE_THRESH_GAUSSIAN_C, cv::THRESH_BINARY, 11, 2); std::vector<std::vector<cv::Point>> contours; cv::findContours(thresh_plate, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); std::vector<cv::Rect> rects; for (int i = 0; i < contours.size(); i++) { cv::Rect rect = cv::boundingRect(contours[i]); if (rect.width > 10 && rect.height > 10 && rect.width < plate_width / 2 && rect.height < plate_height) { rects.push_back(rect); } } std::sort(rects.begin(), rects.end(), [](cv::Rect a, cv::Rect b) { return a.x < b.x; }); for (int i = 0; i < rects.size(); i++) { cv::Mat char_img = gray_plate(rects[i]); cv::resize(char_img, char_img, cv::Size(32, 32)); // 将字符图像转换为一维数组,供后面的神经网络使用 float *char_data = (float *)malloc(32 * 32 * sizeof(float)); for (int row = 0; row < 32; row++) { for (int col = 0; col < 32; col++) { char_data[row * 32 + col] = char_img.at<unsigned char>(row, col) / 255.0; } } // 调用神经网络进行字符识别 int char_label = neural_network(char_data); printf("%c", char_label); free(char_data); } } ``` 4. 将识别结果输出到控制台 ```c++ // 将识别结果输出到控制台 printf("\n"); free(gray_data); } ``` 以上是一个简单的车牌识别算法的示例,可以通过Vivado HLS将其转换为可综合的硬件描述语言,并在FPGA上进行加速。需要注意的是,在实现过程中需要考虑硬件资源的限制和优化算法以提高性能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值