玩嗨 OpenHarmony:基于 OpenHarmony 的简易示波器(下篇)
原文来自HarmonyOS技术社区的老渔翁文章《基于OpenHarmony操作系统的简易示波器开发心得》,原文链接请点击文章结尾的“阅读原文”。
上篇,我们介绍了用OpenHarmony制作简易示波器的一些硬件,包括: 小凌派-RK2206开发板、模数转换芯片ADS1256、Sitronix ST7789V LCD显示器,本篇将继续为大家介绍这个简易示波器到底是如何实现检测峰值、频率,然后进行波形显示的。
LCD显示
LCD型号为ST7789V,采用SPI通信方式,数据传输协议如下:
4-Line Serial Interface => 16-bit/pixel(RGB 5-6-5-bit input),65K-Color,3Ah="05h"
数据传输时序图如下:
LCD使用的是SPI(编者注:串行外设接口Serial Peripheral Interface的缩写)协议,SPI_CS与GPIO0_PC0相连接,SPI_CLK与GPIO0_PC1相连接,SPI_MOSI与GPIO0_PC2相连接,RES与GPIO0_PC3相连接,DC与GPIO0_PC6相连接。
初始化代码分析
本程序可使用SPI或GPIO模拟SPI与LCD进行通信。以下我们主要对SPI与LCD进行通信进行代码分析。这部分代码为SPI初始化的代码。首先用 SpiIoInit() 函数将GPIO0_PC0复用为SPI0_CS0n_M1,GPIO0_PC1复用为SPI0_CLK_M1,GPIO0_PC2复用为SPI0_MOSI_M1。最后调用 LzI2cInit()函数初始化SPI0端口。
if (SpiIoInit(m_spiBus) != LZ_HARDWARE_SUCCESS) {
printf("%s, %d: SpiIoInit failed!\n", __FILE__, __LINE__);
return __LINE__;
}
if (LzSpiInit(LCD_SPI_BUS, m_spiConf) != LZ_HARDWARE_SUCCESS) {
printf("%s, %d: LzSpiInit failed!\n", __FILE__, __LINE__);
return __LINE__;
}
这部分代码为GPIO初始化的代码。首先用 LzGpioInit()函数将GPIO0_PC3初始化为GPIO引脚,然后用 LzGpioSetDir()将引脚设置为输出模式,最后调用 LzGpioSetVal()输出低电平。
/* 初始化GPIO0_C3 */
LzGpioInit(LCD_PIN_RES);
LzGpioSetDir(LCD_PIN_RES, LZGPIO_DIR_OUT);
LzGpioSetVal(LCD_PIN_RES, LZGPIO_LEVEL_HIGH);
/* 初始化GPIO0_C6 */
LzGpioInit(LCD_PIN_DC);
LzGpioSetDir(LCD_PIN_DC, LZGPIO_DIR_OUT);
LzGpioSetVal(LCD_PIN_DC, LZGPIO_LEVEL_LOW);
通过SPI往LCD屏幕写数据操作
这部分的代码是向LCD写入数据,具体如下:
LzSpiWrite(LCD_SPI_BUS, 0, &dat, 1);
配置ST7789V启动
/* 重启lcd */
LCD_RES_Clr();
LOS_Msleep(100);
LCD_RES_Set();
LOS_Msleep(100);
LOS_Msleep(500);
lcd_wr_reg(0x11);
/* 等待LCD 100ms */
LOS_Msleep(100);
/* 启动LCD配置,设置显示和颜色配置 */
lcd_wr_reg(0X36);
if (USE_HORIZONTAL == 0)
{
lcd_wr_data8(0x00);
}
else if (USE_HORIZONTAL == 1)
{
lcd_wr_data8(0xC0);
}
else if (USE_HORIZONTAL == 2)
{
lcd_wr_data8(0x70);
}
else
{
lcd_wr_data8(0xA0);
}
lcd_wr_reg(0X3A);
lcd_wr_data8(0X05);
/* ST7789S帧刷屏率设置 */
lcd_wr_reg(0xb2);
lcd_wr_data8(0x0c);
lcd_wr_data8(0x0c);
lcd_wr_data8(0x00);
lcd_wr_data8(0x33);
lcd_wr_data8(0x33);
lcd_wr_reg(0xb7);
lcd_wr_data8(0x35);
/* ST7789S电源设置 */
lcd_wr_reg(0xbb);
lcd_wr_data8(0x35);
lcd_wr_reg(0xc0);
lcd_wr_data8(0x2c);
lcd_wr_reg(0xc2);
lcd_wr_data8(0x01);
lcd_wr_reg(0xc3);
lcd_wr_data8(0x13);
lcd_wr_reg(0xc4);
lcd_wr_data8(0x20);
lcd_wr_reg(0xc6);
lcd_wr_data8(0x0f);
lcd_wr_reg(0xca);
lcd_wr_data8(0x0f);
lcd_wr_reg(0xc8);
lcd_wr_data8(0x08);
lcd_wr_reg(0x55);
lcd_wr_data8(0x90);
lcd_wr_reg(0xd0);
lcd_wr_data8(0xa4);
lcd_wr_data8(0xa1);
/* ST7789S gamma设置 */
lcd_wr_reg(0xe0);
lcd_wr_data8(0xd0);
lcd_wr_data8(0x00);
lcd_wr_data8(0x06);
lcd_wr_data8(0x09);
lcd_wr_data8(0x0b);
lcd_wr_data8(0x2a);
lcd_wr_data8(0x3c);
lcd_wr_data8(0x55);
lcd_wr_data8(0x4b);
lcd_wr_data8(0x08);
lcd_wr_data8(0x16);
lcd_wr_data8(0x14);
lcd_wr_data8(0x19);
lcd_wr_data8(0x20);
lcd_wr_reg(0xe1);
lcd_wr_data8(0xd0);
lcd_wr_data8(0x00);
lcd_wr_data8(0x06);
lcd_wr_data8(0x09);
lcd_wr_data8(0x0b);
lcd_wr_data8(0x29);
lcd_wr_data8(0x36);
lcd_wr_data8(0x54);
lcd_wr_data8(0x4b);
lcd_wr_data8(0x0d);
lcd_wr_data8(0x16);
lcd_wr_data8(0x14);
lcd_wr_data8(0x21);
lcd_wr_data8(0x20);
lcd_wr_reg(0x29);
这部分代码将ST7789V配置为 4-Line Serial Interface => 16-bit/pixel(RGB 5-6-5-bit input),65K-Color
编译调试
修改 vendor\lockzhiner\rk2206\sample 路径下 BUILD.gn 文件,指定 lcd_example 参与编译。
"./b0_lcd:lcd_example",
修改 device/lockzhiner/rk2206/sdk_liteos 路径下 Makefile 文件,添加 -llcd_example 参与编译。
hardware_LIBS = -lhal_iothardware -lhardware -llcd_example
运行结果
示例代码编译烧录代码后,按下开发板的RESET按键,通过串口助手查看日志,并请使用带有LCD屏幕显示如下:
************Lcd Example***********
************Lcd Example***********
波形显示
通过将采集的幅值进行计算,使最后的值在屏幕大小的范围内,进行描点画图。
void drawCurve( float rawValue,uint16_t color)
{
uint16_t x;
int y;
y = (int) rawValue/30+30; //data processing code
if(y<0 || y > 240)
y = lastY;
//这里之所以是120-rawValue/280,与屏幕的扫描方向有关,如果出现上下颠倒的情况,可以改成120 +
if(firstPoint)//如果是第一次画点,则无需连线,直接描点即可
{
LCD_DrawPoint(0,y,color);
lastX=0;
lastY=y;
firstPoint=0;
}
else
{
x=lastX+2;
if(x<320) //不超过屏幕宽度
{
LCD_DrawLine(lastX,lastY,x,y,color);
lastX=x;
lastY=y;
}
else //超出屏幕宽度,清屏,从第一个点开始绘制,实现动态更新效果
{
//LCD_Fill(0, 0, LCD_W, LCD_H, LCD_WHITE);//清屏//清屏,白色背景
LCD_DrawPoint(0,y,color);
lastX=0;
lastY=y;
}
}
}
峰值检测
通过查找ad采集的数据内的最大值和最小值,然后相减即得峰峰值。
float Get_Vpp(float arr[])
{
uint16_t i;
float MAX=0,MIN=3500,Vpp=0;
for(i=0;i<Ns;i++)
// 扫描ADC数组,获取最大值和最小值
{
if(arr[i]>MAX)
MAX=arr[i];
if(arr[i]<MIN)
MIN=arr[i];
}
Vpp=MAX-MIN;
return Vpp;
}
频率检测
通过fft变换,FFT变换的数据需要两部分,实部和虚部,由于变换的是数据是AD采集的实数据,所以只需将采集的值存入实部,虚部存入零即可。通过变换将时域信号转换到频域,然后通过取模排序,然后计算即可得到频率。他的基本思想是把原始的 N 点序列,依次分解成一系列的短序列。充分利用 DFT 计算式中指数因子所具有的对称性质和周期性质,进而求出这些短序列相应的 DFT 并进行适当组合,达到删除重复计算,减少乘法运算和简化结构的目的。当N是素数时,可以将 DFT算转化为求循环卷积,从而更进一步减少乘法次数,提高速度。
1. FFT变换函数
void fft(const float real_in[], const float imag_in[], float real_out[], float imag_out[], const int n, int isign) {
if (isign != 1 && isign != -1) {//isign=1,正变换;isign=-1,逆变换
return;
}
const int Lv = mylog(n, 2);//蝶形级数
int L;//蝶形运算级数,用于循环
int N;//蝶形运算数据量,用于循环
int distance;//蝶形运算两节点间的距离,用于循环(distance=N/2)
int group;//蝶形运算的组数
float tmpr1, tmpi1, tmpr2, tmpi2;//临时变量
int i, j, k;
for (i = 0; i < n; i++) {//数位倒读
j = rev(i, Lv);
real_out[j] = real_in[i];
imag_out[j] = imag_in[i];
}
L = 1;
distance = 1;
N = 2;
group = n >> 1;
for (; L <= Lv; L++) {//蝶形循环
for (i = 0; i < group; i++) {//每级蝶形各组循环
for (k = 0; k < distance; k++) {//每组蝶形运算
float theta = -2 * PI * k / N * isign;//旋转因子,逆变换的角度与正变换相反
tmpr1 = real_out[N * i + k];
tmpi1 = imag_out[N * i + k];//X(k)
tmpr2 = mycos(theta) * real_out[N * i + k + distance] - mysin(theta) * imag_out[N * i + k + distance];
tmpi2 = mycos(theta) * imag_out[N * i + k + distance] + mysin(theta) * real_out[N * i + k + distance];//WN(k)*X(k+N/2)
real_out[N * i + k] = tmpr1 + tmpr2;
imag_out[N * i + k] = tmpi1 + tmpi2;//X(k)=X(k)+WN(K)*X(k+N/2)
real_out[N * i + k + distance] = tmpr1 - tmpr2;
imag_out[N * i + k + distance] = tmpi1 - tmpi2;//X(k+N/2)=X(k)-WN(K)*X(k+N/2)
if (isign == -1) {//逆变换结果需除以N,即除以Lv次2
real_out[N * i + k] *= 0.5;
imag_out[N * i + k] *= 0.5;
real_out[N * i + k + distance] *= 0.5;
imag_out[N * i + k + distance] *= 0.5;
}
}
}
N <<= 1;
distance <<= 1;
group >>= 1;
}
}
2. 取模运算函数
void PowerMag(void)
{
uint16_t i=0;
float Y,X,Mag;
for (i=0; i < Ns/2; i++)
{
X =((float)y2r[i])/32768* Ns;
Y = ((float)y2i[i])/32768* Ns;
Mag = sqrt(X*X+ Y*Y)/Ns; // 先平方和,再开方
y2[i] = (uint32_t)(Mag*65536);
}
y2[0] = y2[0]/2; //直流
}
3. 峰值排序
然后将FFT变换的幅值进行排序,同时也对他们的下标进行了排序,以便后续的计算,即除了直流信号的第一个频率点即为改信号的频率。
void sorting (void)
{
uint16_t i,j;
uint32_t temp1;
for(i=0;i<Ns/2;i++) //下标赋初值
{
xb[i]=i;
}
for(j=0;j<(Ns/2-1);j++) // 冒泡排序
{
for(i=1;i<(Ns/2-j-1);i++) //直流项不参与排序 从第二项开始
{
if(y2[i]<y2[i+1])
{
temp1=y2[i]; //交换数据
y2[i]=y2[i+1];
y2[i+1]=temp1;
temp1=xb[i]; //交换下标
xb[i]=xb[i+1];
xb[i+1]=temp1;
}
}
}
}
4. 计算频率
通过计算即可得到频率,采样点数将采样频率进行平分,通过排序取得的幅值最大的那个点的下标进行相乘即为频率,1.47为补偿系数,因为ADS1256采集数据后有延时,导致进行FFT变换后所对应的幅值最大点的下标前移,导致计算频率时候会偏小。
fre=Fs*xb[1]*1.47/Ns;
心得体会
通过OpenHarmony操作系统 + 小凌派-RK2206开发板进行项目开发,OpenHarmony的实时性好,稳定性高,瑞芯微RK2206芯片接口比较丰富,移植适配稳定性较好,整体开发进度比较顺利,开发的难度都集中在数据处理算法上。通过这一次简易示波器的应用开发,整体上对OpenHarmony和国产芯片开发还是蛮认可的,是一次不错的学习体验!
写在最后
我们最近正带着大家玩嗨OpenHarmony。如果你有好玩的东东,欢迎投稿,让我们一起嗨起来!有点子,有想法,有Demo,立刻联系我们:
合作邮箱:zzliang@atomsource.org