第5章〓基础外设应用
5.1点亮LED灯◆

在嵌入式开发中,I/O口的高低电平控制是最简单的外设控制之一。本节通过一个典型的点亮LED灯实验,带大家开启OpenHarmony基础外设开发之旅。通过本节的学习,可以了解RK2206芯片的I/O口控制使用方法。

RK2206芯片可以提供多达32个双向GPIO口,它们分别分布在PA~PD这4个端口中,每个端口有8个GPIO,每个GPIO口都可以承受最大3.3V的压降。通过RK2206芯片寄存器配置,可以将GPIO口配置成想要的工作模式。

5.1.1硬件电路设计

模块硬件电路如图5.1.1所示,可以看到LED灯引脚连接到RK2206芯片的GPIO0_D3。



图5.1.1模块硬件电路图


5.1.2程序设计

通过初始化LED灯对应的GPIO口,然后每隔1s控制GPIO输出电平点亮或熄灭LED灯。

1.  主程序设计

如图5.1.2所示为点亮LED灯的主程序流程图。LiteOS系统初始化后,再初始化LED灯的GPIO口,并使变量i为0,最后进入死循环。死循环中根据变量i控制GPIO口输出电平,如果i=0,则输出低电平,设置i=1; 如果i=1,则输出高电平,设置i=0; 最后睡眠1s。



图5.1.2主程序流程图


2.  GPIO初始化程序设计

GPIO初始化程序主要分为I/O口初始化和控制I/O口输出高电平两部分。

void led_init()

{

/* 配置GPIO0_PD3的复用功能寄存器为GPIO */

PinctrlSet(GPIO0_PD3, MUX_FUNC0, PULL_KEEP, DRIVE_KEEP);

/* 初始化GPIO0_PD3 */

LzGpioInit(GPIO0_PD3);

/* 设置GPIO0_PD3为输出模式 */

LzGpioSetDir(GPIO0_PD3, LZGPIO_DIR_OUT);

/* 设置GPIO0_PD3输出低电平 */

LzGpioSetVal(GPIO0_PD3, LZGPIO_LEVEL_LOW);

}




3.  GPIO控制LED亮灭程序设计

在死循环中,第1秒LED灯灭,第2秒LED灯亮,如此反复。

void task_led()

{

uint8_t i;



/* 初始化LED灯的GPIO引脚 */

led_init();

i = 0;



while (1)

{

if (i == 0)

{

printf("Led Off\n");

/* 控制GPIO0_PD3输出低电平 */

LzGpioSetVal(GPIO0_PD3, LZGPIO_LEVEL_LOW);

i = 1;

}

else

{

printf("Led On\n");

/* 控制GPIO0_PD3输出高电平 */

LzGpioSetVal(GPIO0_PD3, LZGPIO_LEVEL_HIGH);

i = 0;

}



/* 睡眠1s。该函数为OpenHarmony内核睡眠函数,单位:ms */

LOS_Msleep(1000);

}

}





5.1.3实验结果

程序编译烧写到开发板后,按下开发板的RESET按键,通过串口软件查看日志,程序代码如下: 

Led Off

Led On

Led Off

Led On

...




5.2ADC按键◆

在嵌入式系统产品开发中,按键板的设计是最基本的,也是项目评估阶段必须要考虑的问题。其实现方式有很多种,具体使用哪一种需要结合可用I/O数量和成本,做出最终选择。传统的按键检测方法是一个按键对应一个GPIO口,进行高低电平输入检测。可是在GPIO口紧缺的情况下,就需要一个有效的解决方案,其中ADC检测实现按键功能是一种相对有效的解决方案。

ADC检测实现简单实用的按键方法: 仅需要一个ADC和若干电阻就可实现多个按键的输入检测。ADC检测的工作原理: 按下按键时,通过电阻分压得到不同的电压值,ADC采集在各个范围内的值来判定是哪个按键被按下。

5.2.1硬件电路设计

模块整体硬件电路图如图5.2.1所示,电路中包含了1个ADC引脚和4个按键,USER_KEY_ADC引脚连接到RK2206芯片的GPIO0_C5。



图5.2.1模块整体硬件电路图


其中,4个按键分别连接不同的电阻。当按键被按下时,USER_KEY_ADC检测到不同的电压。按键对应电压表如表5.2.1所示。


表5.2.1按键对应电压表



序号按键电压/V
1K10.01
2K20.55
3K31.00
4K41.65





视频讲解

5.2.2程序设计

ADC按键程序每1s通过GPIO0_PC5读取一次按键电压,通过电压数值判断当前是哪个按键被按下,并打印出该按键名称。

1. 主程序设计

如图5.2.2所示为ADC按键主程序流程图,开机LiteOS系统初始化后,进入主程序,先初始化ADC设备。程序进入主循环,1s获取一次ADC采样电压,判断: 



图5.2.2ADC按键主程序流程图


(1) 若采样电压为0~0.11V,则当前是按下K1,打印按键Key1; 

(2) 若采样电压为0.45~0.65V,则当前是按下K2,打印按键Key2; 

(3) 若采样电压为0.9~1.1V,则当前是按下K3,打印按键Key3; 

(4) 若采样电压为1.55~1.75V,则当前是按下K4,打印按键Key4; 

(5) 当前无按键。

void adc_process()

{

float voltage;



/* 初始化ADC设备 */

adc_dev_init();



while (1)

{

printf("***************Adc Example*************\r\n");

/*获取电压值*/

voltage = adc_get_voltage();

printf("vlt:%.3fV\n", voltage);



if ((0.11 >= voltage) && (voltage >= 0.00))






{

printf("\tKey1\n");

}

else if ((0.65 >= voltage) && (voltage >= 0.45))

{

printf("\tKey2\n");

}

else if ((1.1 >= voltage) && (voltage >= 0.9))

{

printf("\tKey3\n");

}

else if ((1.75 >= voltage) && (voltage >= 1.55))

{

printf("\tKey4\n");

}



/* 睡眠1s */

LOS_Msleep(1000);

}

}





2. ADC初始化程序设计

ADC初始化程序主要分为ADC初始化和配置ADC参考电压为外部电压两部分。

static unsigned int adc_dev_init()

{

unsigned int ret = 0;

uint32_t *pGrfSocCon29 = (uint32_t *)(0x41050000U + 0x274U);

uint32_t ulValue;



ret = DevIoInit(m_adcKey);

if (ret != LZ_HARDWARE_SUCCESS)

{

printf("%s, %s, %d: ADC Key IO Init fail\n", __FILE__, __func__, __LINE__);

return __LINE__;

}

ret = LzSaradcInit();

if (ret != LZ_HARDWARE_SUCCESS) {

printf("%s, %s, %d: ADC Init fail\n", __FILE__, __func__, __LINE__);

return __LINE__;

}



/* 设置saradc的电压信号,选择AVDD */

ulValue = *pGrfSocCon29;

ulValue &= ~(0x1 << 4);

ulValue |= ((0x1 << 4) << 16);

*pGrfSocCon29 = ulValue;

    

return 0;

}





3. ADC读取电压程序设计

RK2206芯片采用一种逐次逼近寄存器型模数转换器(SuccessiveApproximation Analog to Digital Converter),是一种常用的A/D转换结构,其功耗较低,转换速率较高,在有低功耗要求(可穿戴设备、物联网)的数据采集场景下应用广泛。该ADC采用10位采样,最高电压为3.3V。简言之,ADC采样读取的数据,位0~位9有效,且最高数值0x400(即1024)代表实际电压差3.3V,也就是说,1个数值等于3.3V/1024≈0.003223V。

static float adc_get_voltage()

{

unsigned int ret = LZ_HARDWARE_SUCCESS;

unsigned int data = 0;

ret = LzSaradcReadValue(ADC_CHANNEL, &data);

if (ret != LZ_HARDWARE_SUCCESS)

{

printf("%s, %s, %d: ADC Read Fail\n", __FILE__, __func__, __LINE__);

return 0.0;

}



return (float)(data * 3.3 / 1024.0);

}




5.2.3实验结果

程序编译烧写到开发板后,按下开发板的RESET按键,通过串口软件查看日志,程序代码如下: 

***************Adc Example*************

vlt:3.297V

***************Adc Example*************

vlt:1.67V

Key4




5.3LCD显示◆

LCD的应用很广泛,简单如手表上的显示屏,仪表仪器上的显示器或者是笔记本电脑上的显示器,都使用了LCD。在一般的办公设备上也很常见,如传真机、复印机,在一些娱乐器材等上也常常见到LCD的身影。

本节使用的LCD采用ST7789V驱动器,可单片驱动262K色图像的TFTLCD,包含720(240×3色)×320线输出,可以直接以SPI协议,以及8位/9位/16位/18位并行连接外部控制器。ST7789V显示数据存储在片内240×320×18位内存中,显示内存的读写不需要外部时钟驱动。关于ST7789V驱动器的详细内容可以查看其芯片手册。

5.3.1硬件电路设计

模块整体硬件电路图如图5.3.1所示,电路中包含了电源电路、液晶接口以及小凌派RK2206开发板连接的相关引脚。其中,液晶屏ST7789V的相关引脚资源如图5.3.2所示。



图5.3.1模块整体硬件电路图





图5.3.2液晶屏ST7789V的相关引脚资源


LCD引脚功能描述如表5.3.1所示。


表5.3.1LCD引脚功能描述



序号LCD引脚功 能 描 述

1
D/C
指令/数据选择端,L: 指令,H: 数据
2
RESET
复位信号线,低电平有效
3
SPI_MOSI
SPI数据输入信号线
4
SPI_CLK
SPI时钟信号线
5
SPI_CS
SPI片选信号线,低电平有效
6
GND
电源地引脚
7
5V
5V电源输入引脚


2.4寸LCD和小凌派RK2206开发板连接图如图5.3.3所示。



视频讲解

5.3.2程序设计

本节将利用小凌派RK2206开发板上的GPIO和SPI接口方式来点亮2.4寸LCD,并实现ASCII字符及汉字的显示。

1.  主程序设计

如图5.3.4所示为LCD主程序流程图,开机LiteOS系统初始化后,进入主程序。主程序首先进行GPIO和SPI总线初始化,然后配置LCD设备,最后进入循环。在循环中,主程序控制SPI对LCD进行ASCII字符和汉字的显示。



图5.3.32.4寸LCD和小凌派RK2206开发板连接图




图5.3.4LCD主程序流程图



2.  LCD初始化程序设计

LCD初始化程序主要分为GPIO和SPI总线初始化及配置LCD两部分。

其中,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初始化首先用SpiIoInit()函数将GPIO0_PC0复用为SPI0_CS0n_M1,GPIO0_PC1复用为SPI0_CLK_M1,GPIO0_PC2复用为SPI0_MOSI_M1。其次调用LzI2cInit()函数初始化SPI0端口。

LzSpiDeinit(LCD_SPI_BUS);



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__;

}




配置LCD主要是配置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);

/* ST7789V帧刷屏率设置 */

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);

/* ST7789V电源设置 */

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);

/* ST7789V 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);




3.  LCD的点数据设计

ST7789V采用4线串行SPI通信方式,数据位共16位,其RGB分别是5位、6位和5位,也就是共65K个颜色,寄存器3AH的值设置为05H。ST7789V液晶屏SPI数据传输时序图如图5.3.5所示。

也就是一个像素点的RGB为5位+6位+5位,每个像素点需要占用2字节存储空间。因此,向LCD发送某个像素信息的程序代码如下: 

static void lcd_write_bus(uint8_t dat)

{

LzSpiWrite(LCD_SPI_BUS, 0, &dat, 1);

}



static void lcd_wr_data(uint16_t dat)

{

lcd_write_bus(dat >> 8);

lcd_write_bus(dat);

}



static void lcd_wr_reg(uint8_t dat)

{

LCD_DC_Clr();

lcd_write_bus(dat);

LCD_DC_Set();

}



static void lcd_address_set(uint16_t x1,uint16_t y1,uint16_t x2,uint16_t y2)

{

/* 列地址设置 */

lcd_wr_reg(0x2a);

lcd_wr_data(x1);

lcd_wr_data(x2);

/* 行地址设置 */

lcd_wr_reg(0x2b);

lcd_wr_data(y1);

lcd_wr_data(y2);

/* 写存储器 */

lcd_wr_reg(0x2c);

}



static void lcd_wr_data(uint16_t dat)

{

lcd_write_bus(dat >> 8);

lcd_write_bus(dat);

}



void lcd_draw_point(uint16_t x, uint16_t y, uint16_t color)

{

/* 设置光标位置 */

lcd_address_set(x, y, x, y);

lcd_wr_data(color);

}











图5.3.5ST7789V液晶屏SPI数据传输时序图



4.  LCD的ASCII字符显示设计

预先将规定字号的ASCII字符的LCD像素信息存放于在lcd_font.h源代码文件中。LCD依照ASCII的数值来存放像素信息。例如,空格的ASCII数值是0x0,则程序将像素放到第一行像素中,具体代码如下: 

/* 12*6的ASCII码显示 */

const unsigned char ascii_1206[][12] =

{

{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, /*" ",0*/

{0x00, 0x00, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x00, 0x04, 0x00, 0x00}, /*"!",1*/

{0x14, 0x14, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, /*""",2*/

{0x00, 0x00, 0x0A, 0x0A, 0x1F, 0x0A, 0x0A, 0x1F, 0x0A, 0x0A, 0x00, 0x00}, /*"#",3*/

{0x00, 0x04, 0x0E, 0x15, 0x05, 0x06, 0x0C, 0x14, 0x15, 0x0E, 0x04, 0x00}, /*"$",4*/

... 

};



/* 16*8的ASCII码显示 */

const unsigned char ascii_1608[][16] =

{

{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, /*" ",0*/

{0x00, 0x00, 0x00, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00}, /*"!",1*/

{0x00, 0x48, 0x6C, 0x24, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, /*""",2*/

{0x00, 0x00, 0x00, 0x24, 0x24, 0x24, 0x7F, 0x12, 0x12, 0x12, 0x7F, 0x12, 0x12, 0x12, 0x00, 0x00}, /*"#",3*/

... };



/* 24*12的ASCII码显示 */

const unsigned char ascii_2412[][48] =

{

{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, /*" ",0*/

{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x40, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, /*"!",1*/

{0x00, 0x00, 0x00, 0x00, 0x60, 0x06, 0x60, 0x06, 0x30, 0x03, 0x98, 0x01, 0x88, 0x00, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, /*""",2*/

...

};





当需要将某个字号的ASCII字符投射到LCD时,程序根据字号大小找到对应的ASCII字符像素表,然后根据ASCII字符的数值找到对应的像素行,最后将该像素行数据依次通过SPI总线发送给LCD,具体代码如下: 



void lcd_show_char(uint16_t x, uint16_t y, uint8_t num, uint16_t fc, uint16_t bc, uint8_t sizey, uint8_t mode)

{

uint8_t temp,sizex,t,m = 0;

uint16_t i;

uint16_t TypefaceNum;   //一个字符所占字节大小

uint16_t x0 = x;

    

sizex = sizey/2;

TypefaceNum = (sizex/8 + ((sizex%8)?1:0)) * sizey;



/* 得到偏移后的值 */

num = num-' ';

/* 设置光标位置 */

lcd_address_set(x, y, x+sizex-1, y+sizey-1);

    

for (i = 0; i < TypefaceNum; i++)

{ 

if (sizey == 12)

{

/* 调用6x12字体 */

temp = ascii_1206[num][i];

}

else if (sizey == 16)

{

/* 调用8x16字体 */

temp = ascii_1608[num][i];

}

else if (sizey == 24)

{

/* 调用12x24字体 */

temp = ascii_2412[num][i];

}

else if (sizey == 32)

{

/* 调用16x32字体 */

temp = ascii_3216[num][i];

}

else

{

return;

}



for (t = 0; t < 8; t++)

{

if (!mode)

{/* 非叠加模式 */

if (temp & (0x01 << t))

{

lcd_wr_data(fc);

}

else

{

lcd_wr_data(bc);

}



m++;

if (m%sizex == 0)






{

m = 0;

break;

}

}

else

{/* 叠加模式 */

if (temp & (0x01 << t))

{

/* 画一个点 */

lcd_draw_point(x, y, fc);

}



x++;

if ((x - x0) == sizex)

{

x = x0;

y++;

break;

}

}

}

}

}





5.  LCD的汉字显示设计

原理同上,程序将某一个特定字号的汉字信息存放于一个数据结构体数组中。该数据结构体包含字体编码Index和像素数据Msk,具体代码如下: 

/* 定义中文字符 12*12 */

typedef struct

{

unsigned char Index[2];

unsigned char Msk[24];

} typFNT_GB12;



/* 定义中文字符 16*16 */

typedef struct

{

unsigned char Index[2];

unsigned char Msk[32];

} typFNT_GB16;



/* 定义中文字符 24*24 */

typedef struct

{

unsigned char Index[2];

unsigned char Msk[72];

} typFNT_GB24;

...




通过汉字像素软件将对应的汉字和像素存放于lcd_font.h文件中,具体代码如下: 

const typFNT_GB12 tfont12[] =

{

"小", 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x20, 0x00, 0x24, 0x01, 0x24, 0x02, 0x22, 0x02, 0x22, 0x04, 0x21, 0x04, 0x20, 0x00, 0x20, 0x00, 0x38, 0x00, /*"小"*/






"凌", 0x40, 0x00, 0xF9, 0x03, 0x42, 0x00, 0xFC, 0x07, 0x10, 0x01, 0x28, 0x02, 0xE0, 0x01, 0x14, 0x01, 0xAA, 0x00, 0x41, 0x00, 0xB0, 0x01, 0x0C, 0x06, /*"凌"*/



"派", 0x00, 0x03, 0xF2, 0x00, 0x14, 0x02, 0xD0, 0x01, 0x51, 0x01, 0x52, 0x05, 0x50, 0x03, 0x50, 0x01, 0x54, 0x01, 0x52, 0x02, 0xD1, 0x02, 0x48, 0x04, /*"派"*/



};



const typFNT_GB16 tfont16[] =

{

"小", 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x88, 0x08, 0x88, 0x10, 0x88, 0x20, 0x84, 0x20, 0x84, 0x40, 0x82, 0x40, 0x81, 0x40, 0x80, 0x00, 0x80, 0x00, 0xA0, 0x00, 0x40, 0x00, /*"小",0*/


"凌", 0x00, 0x02, 0x02, 0x02, 0xC4, 0x1F, 0x04, 0x02, 0x00, 0x02, 0xE0, 0x7F, 0x88, 0x08, 0x48, 0x11, 0x24, 0x21, 0x87, 0x0F, 0xC4, 0x08, 0x24, 0x05, 0x04, 0x02, 0x04, 0x05, 0xC4, 0x08, 0x30, 0x30, /*"凌",1*/



"派", 0x00, 0x10, 0x04, 0x3C, 0xE8, 0x03, 0x28, 0x00, 0x21, 0x38, 0xA2, 0x07, 0xA2, 0x04, 0xA8, 0x44, 0xA8, 0x24, 0xA4, 0x14, 0xA7, 0x08, 0xA4, 0x08, 0xA4, 0x10, 0x94, 0x22, 0x94, 0x41, 0x88, 0x00, /*"派",2*/



};

...




当程序需要将某个特定字号的汉字投射到LCD时,程序就根据对应的字号查找对应字号的tfontXX数组,并将对应的像素行数据发送给LCD,具体代码如下: 

void lcd_show_chinese(uint16_t x, uint16_t y, uint8_t *s, uint16_t fc, uint16_t bc, uint8_t sizey, uint8_t mode)

{

uint8_t buffer[128];

uint32_t buffer_len = 0;

uint32_t len = strlen(s);



memset(buffer, 0, sizeof(buffer));

/* UTF8格式汉字转换为ASCII格式 */

chinese_utf8_to_ascii(s, strlen(s), buffer, &buffer_len);



for (uint32_t i = 0; i < buffer_len; i += 2, x += sizey)

{

if (sizey == 12)

{

lcd_show_chinese_12x12(x, y, &buffer[i], fc, bc, sizey, mode);

}

else if (sizey == 16)

{

lcd_show_chinese_16x16(x, y, &buffer[i], fc, bc, sizey, mode);

}

else if (sizey == 24)

{

lcd_show_chinese_24x24(x, y, &buffer[i], fc, bc, sizey, mode);

}

else if (sizey == 32)

{

lcd_show_chinese_32x32(x, y, &buffer[i], fc, bc, sizey, mode);






}

else

{

return;

}

}

}





5.3.3实验结果

程序编译烧写到开发板后,按下开发板的RESET按键,通过串口软件查看日志,程序代码如下: 

************Lcd Example***********



************Lcd Example***********




5.4EEPROM应用◆

在实际应用中,保存在RAM中的数据掉电后就丢失了,保存在Flash中的数据又不能随意改变,也就是不能用它来记录变化的数值。在某些特定场合,需要记录下某些数据,并且它们时常会改变或更新,掉电之后数据还不能丢失。例如,家用电表度数、电视机的频道记忆,一般都是使用EEPROM(ElectricallyErasable Programmable ReadOnly Memory,电擦除可编程只读存储器)来保存数据的,其特点就是掉电后存储的数据不丢失。

EEPROM是一种掉电后数据不丢失的存储芯片。可以通过计算机或专用设备擦除EEPROM的已有信息,重新编程。一般情况下,EEPROM拥有30万~100万次的寿命,也就是它可以反复写入30万~100万次,而读取次数是无限的。

5.4.1硬件电路设计

以人体感应模块为例,整体硬件电路图如图5.4.1所示,电路中包含了E53接口连接器和EEPROM。



图5.4.1整体硬件电路图



设计使用的EEPROM型号是K24C02,它是一个常用的基于I2C通信协议的EEPROM元件,例如,ATMEL公司的AT24C02、CATALYST公司的CAT24C02和ST公司的ST24C02等芯片。I2C是一个通信协议,它拥有严密的通信时序逻辑要求,而EEPROM是一个元件,只是这个元件采用了I2C协议的接口与单片机相连,二者并没有必然的联系,EEPROM可以用其他接口,I2C也可以用在其他很多元件上。根据K24C02芯片手册,可获取如下信息。

1. K24C02芯片的从设备地址

因其存储容量为2Kb,所以该芯片I2C从设备读写地址分别为51H和50H,如图5.4.2所示。



图5.4.2K24C02的从设备地址图


2. K24C02芯片的读操作

K24C02芯片的读操作共分为3种,分别为当前地址读(Current Address Read)、随机读(Random Read)和连续读(Sequential Read)。

当前地址读(Current Address Read)操作是控制I2C总线与K24C02芯片通信,通信内容为: 从设备地址(1字节,最低位为1,表示读) + 数据(1字节,K24C02发送给CPU的存储内容)。该读操作没有附带EEPROM的存储地址,存储地址是由上一次存储地址累加而来,如图5.4.3所示。



图5.4.3K24C02的当前地址读操作


而随机读操作则控制I2C与K24C02进行两次通信: 

第一次I2C通信: 从设备地址(1字节,最低位为0,表示写)+存储地址(1字节,CPU发送给K24C02的存储地址)。

第二次I2C通信: 从设备地址(1字节,最低位为1,表示读)+数据(1字节,K24C02发送给CPU的存储内容)。

K24C02的随机地址读操作数据传输如图5.4.4所示。



图5.4.4K24C02的随机地址读操作数据传输


连续读操作(Sequential Read)则控制I2C往K24C02发送n字节,通信内容为: 从设备地址(1字节,最低位为1,表示读)+n个数据(K24C02发送给CPU)。K24C02的连续读操作数据传输如图5.4.5所示。



图5.4.5K24C02的连续读操作数据传输


3. K24C02芯片的写操作

K24C02芯片写数据操作可分为两种,分别为字节写操作(Byte Write)和页写操作(Page Write)。



其中,字节写操作(Byte Write)控制I2C与K24C02通信,通信内容为: 从设备地址(1字节,最低位为0,表示写)+存储地址(1字节)+数据(1字节,CPU发送给K24C0的存储内容)。K24C02的字节写操作数据传输如图5.4.6所示。



图5.4.6K24C02的字节写操作数据传输


页写操作(Page Write)则控制I2C与K24C02通信,通信内容为: 从设备地址(1字节,最低位为0,表示写)+存储地址(1字节)+数据(n字节,CPU发送给K24C0的存储内容)。其中,存储数据的n字节,n不能超过页大小(K24C02的页大小为8字节)。

小凌派RK2206开发板与人体感应模块均带有防呆设计,故很容易区分安装方向,直接将模块插入开发板的E53母座接口上即可,如图5.4.7所示。



图5.4.7硬件连接图




图5.4.8EEPROM存储主

程序流程图

5.4.2程序设计

可通过程序控制RK2206芯片的I2C与K24C02芯片通信,每5s向某一块存储空间(该存储空间地址依次累加)写入不同数据,然后再读取出来。

1.  主程序设计

如图5.4.8所示为EEPROM存储主程序流程图。主程序首先初始化I2C总线,接着程序进入主循环,每5s将不同的数据写入一块存储空间,然后再读取出来。其中,存储空间地址每次循环都累加32,数据也随着循环而累加1。

while (1)

{

printf("************ EEPROM Process ************\n");

printf("BlockSize = 0x%x\n", eeprom_get_blocksize());



/* 写EEPROM */

memset(buffer, 0, sizeof(buffer));

for (unsigned int i = 0; i < FOR_CHAR; i++)

{

buffer[i] = data_offset + i;

printf("Write Byte: %d = %c\n", addr_offset + i, buffer[i]);

}

ret = eeprom_write(addr_offset, buffer, FOR_CHAR);

if (ret != FOR_CHAR)

{

printf("EepromWrite failed(%d)\n", ret);

}

        

/* 读EEPROM */

memset(buffer, 0, sizeof(buffer));

ret = eeprom_read(addr_offset, buffer, FOR_CHAR);

if (ret != FOR_CHAR)

{

printf("Read Bytes: failed!\n");

}

else






{

for (unsigned int i = 0; i < FOR_CHAR; i++)

{

printf("Read Byte: %d = %c\n", addr_offset + i, buffer[i]);

}

}

        

data_offset++;

if (data_offset >= CHAR_END)

{

data_offset = CHAR_START;

}

        

addr_offset += FOR_ADDRESS;

if (addr_offset >= 200)

{

addr_offset = 0;

}

printf("\n");

        

LOS_Msleep(5000);

}





2. EEPROM初始化程序设计

主程序通过控制RK2206芯片的接口对I2C总线进行初始化。

#define EEPROM_I2C_BUS0

#define EEPROM_I2C_ADDRESS0x51



static I2cBusIo m_i2cBus = {

.scl =  {.gpio = GPIO0_PA1, .func = MUX_FUNC3, .type = PULL_NONE, .drv = DRIVE_KEEP, .dir = LZGPIO_DIR_KEEP, .val = LZGPIO_LEVEL_KEEP},

.sda =  {.gpio = GPIO0_PA0, .func = MUX_FUNC3, .type = PULL_NONE, .drv = DRIVE_KEEP, .dir = LZGPIO_DIR_KEEP, .val = LZGPIO_LEVEL_KEEP},

.id = FUNC_ID_I2C0,

.mode = FUNC_MODE_M2,

};



static unsigned int m_i2c_freq = 100000;





unsigned int eeprom_init()

{

if (I2cIoInit(m_i2cBus) != LZ_HARDWARE_SUCCESS) {

printf("%s, %d: I2cIoInit failed!\n", __FILE__, __LINE__);

return __LINE__;

}

if (LzI2cInit(EEPROM_I2C_BUS, m_i2c_freq) != LZ_HARDWARE_SUCCESS) {

printf("%s, %d: I2cInit failed!\n", __FILE__, __LINE__);

return __LINE__;

}



/* GPIO0_A0 => I2C1_SDA_M1 */

PinctrlSet(GPIO0_PA0, MUX_FUNC3, PULL_NONE, DRIVE_KEEP);






/* GPIO0_A1 => I2C1_SCL_M1 */

PinctrlSet(GPIO0_PA1, MUX_FUNC3, PULL_NONE, DRIVE_KEEP);



return 0;

}





3.  EEPROM读操作程序设计

主程序通过eeprom_read()控制I2C总线与EEPROM进行通信,读取EEPROM存储内容。其中,eeprom_readbyte()表示通过I2C总线读取EEPROM存储器1字节。



#define EEPROM_I2C_BUS0

#define EEPROM_I2C_ADDRESS0x51



/* EEPROM型号:K24C02,2Kb(256B),32页,每页8字节*/

#define EEPROM_ADDRESS_MAX256

#define EEPROM_PAGE8



unsigned int eeprom_readbyte(unsigned int addr, unsigned char *data)

{

unsigned int ret = 0;

unsigned char buffer[1];

LzI2cMsg msgs[2];



/* K24C02的存储地址是0~255 */

if (addr >= EEPROM_ADDRESS_MAX) {

printf("%s, %s, %d: addr(0x%x) >= EEPROM_ADDRESS_MAX(0x%x)\n", __FILE__, __func__, __LINE__, addr, EEPROM_ADDRESS_MAX);

return 0;

}



buffer[0] = (unsigned char)addr;

msgs[0].addr = EEPROM_I2C_ADDRESS;

msgs[0].flags = 0;

msgs[0].buf = &buffer[0];

msgs[0].len = 1;

msgs[1].addr = EEPROM_I2C_ADDRESS;

msgs[1].flags = I2C_M_RD;

msgs[1].buf = data;

msgs[1].len = 1;

ret = LzI2cTransfer(EEPROM_I2C_BUS, msgs, 2);

if (ret != LZ_HARDWARE_SUCCESS) {

printf("%s, %s, %d: LzI2cTransfer failed(%d)!\n", __FILE__, __func__, __LINE__, ret);

return 0;

}



return 1;

}



unsigned int eeprom_read(unsigned int addr, unsigned char *data, unsigned int data_len) 

{

unsigned int ret = 0;



if (addr >= EEPROM_ADDRESS_MAX) {

printf("%s, %s, %d: addr(0x%x) >= EEPROM_ADDRESS_MAX(0x%x)\n", __FILE__, __func__, __LINE__, addr, EEPROM_ADDRESS_MAX);







return 0;

}



if ((addr + data_len) > EEPROM_ADDRESS_MAX) {

printf("%s, %s, %d: addr + len(0x%x) > EEPROM_ADDRESS_MAX(0x%x)\n", __FILE__, __func__, __LINE__, addr + data_len, EEPROM_ADDRESS_MAX);

return 0;

}   



ret = eeprom_readbyte(addr, data);

if (ret != 1) {

printf("%s, %s, %d: EepromReadByte failed(%d)\n", __FILE__, __func__, __LINE__, ret);

return 0;

}



if (data_len > 1) {

ret = LzI2cRead(EEPROM_I2C_BUS, EEPROM_I2C_ADDRESS, &data[1], data_len - 1);

if (ret < 0) {

printf("%s, %s, %d: LzI2cRead failed(%d)!\n", __FILE__, __func__, __LINE__, ret);

return 0;

}

}



return data_len;

}





4.  EEPROM写操作程序设计

主程序根据存储地址、存储数据和数据长度的不同,选用字节写操作或页写操作,具体代码如下: 

#define EEPROM_I2C_BUS0

#define EEPROM_I2C_ADDRESS0x51



/* EEPROM型号:K24C02,2Kb(256B),32页,每页8字节*/

#define EEPROM_ADDRESS_MAX256

#define EEPROM_PAGE8



unsigned int eeprom_writebyte(unsigned int addr, unsigned char data)

{

unsigned int ret = 0;

LzI2cMsg msgs[1];

unsigned char buffer[2];

/* K24C02的存储地址是0~255 */

if (addr >= EEPROM_ADDRESS_MAX) {

printf("%s, %s, %d: addr(0x%x) >= EEPROM_ADDRESS_MAX(0x%x)\n", __FILE__, __func__, __LINE__, addr, EEPROM_ADDRESS_MAX);

return 0;

}



buffer[0] = (unsigned char)(addr & 0xFF);

buffer[1] = data;

msgs[0].addr = EEPROM_I2C_ADDRESS;

msgs[0].flags = 0;






msgs[0].buf = &buffer[0];

msgs[0].len = 2;

ret = LzI2cTransfer(EEPROM_I2C_BUS, msgs, 1);

if (ret != LZ_HARDWARE_SUCCESS) {

printf("%s, %s, %d: LzI2cTransfer failed(%d)!\n", __FILE__, __func__, __LINE__, ret);

return 0;

}



/* K24C02芯片需要时间完成写操作,在此之前不响应其他操作*/

eeprog_delay_usec(1000);

return 1;

}



unsigned int eeprom_writepage(unsigned int addr, unsigned char *data, unsigned int data_len)

{

unsigned int ret = 0;

LzI2cMsg msgs[1];

unsigned char buffer[EEPROM_PAGE + 1];

    

/* K24C02的存储地址是0~255 */

if (addr >= EEPROM_ADDRESS_MAX) {

printf("%s, %s, %d: addr(0x%x) >= EEPROM_ADDRESS_MAX(0x%x)\n", __FILE__, __func__, __LINE__, addr, EEPROM_ADDRESS_MAX);

return 0;

}



if ((addr % EEPROM_PAGE) != 0) {

printf("%s, %s, %d: addr(0x%x) is not page addr(0x%x)\n", __FILE__, __func__, __LINE__, addr, EEPROM_PAGE);

return 0;

}



if ((addr + data_len) > EEPROM_ADDRESS_MAX) {

printf("%s, %s, %d: addr + data_len(0x%x) > EEPROM_ADDRESS_MAX(0x%x)\n", __FILE__, __func__, __LINE__, addr + data_len, EEPROM_ADDRESS_MAX);

return 0;

}



if (data_len > EEPROM_PAGE) {

printf("%s, %s, %d: data_len(%d) > EEPROM_PAGE(%d)\n", __FILE__, __func__, __LINE__, data_len, EEPROM_PAGE);

return 0;

}



buffer[0] = addr;

memcpy(&buffer[1], data, data_len);

msgs[0].addr = EEPROM_I2C_ADDRESS;

msgs[0].flags = 0;

msgs[0].buf = &buffer[0];

msgs[0].len = 1 + data_len;

ret = LzI2cTransfer(EEPROM_I2C_BUS, msgs, 1);

if (ret != LZ_HARDWARE_SUCCESS) {

printf("%s, %s, %d: LzI2cTransfer failed(%d)!\n", __FILE__, __func__, __LINE__, ret);

return 0;

}







/* K24C02芯片需要时间完成写操作,在此之前不响应其他操作*/

eeprog_delay_usec(1000);

return data_len;

}



unsigned int eeprom_write(unsigned int addr, unsigned char *data, unsigned int data_len)

{

unsigned int ret = 0;

unsigned int offset_current = 0;

unsigned int page_start, page_end;

unsigned char is_data_front = 0;

unsigned char is_data_back = 0;

unsigned int len;



if (addr >= EEPROM_ADDRESS_MAX) {

printf("%s, %s, %d: addr(0x%x) >= EEPROM_ADDRESS_MAX(0x%x)\n", __FILE__, __func__, __LINE__, addr, EEPROM_ADDRESS_MAX);

return 0;

}



if ((addr + data_len) > EEPROM_ADDRESS_MAX) {

printf("%s, %s, %d: addr + len(0x%x) > EEPROM_ADDRESS_MAX(0x%x)\n", __FILE__, __func__, __LINE__, addr + data_len, EEPROM_ADDRESS_MAX);

return 0;


}



/* 判断addr是否为页地址 */

page_start = addr / EEPROM_PAGE;

if ((addr % EEPROM_PAGE) != 0) {

page_start += 1;

is_data_front = 1;

}



/* 判断addr + data_len是否为页地址 */

page_end = (addr + data_len) / EEPROM_PAGE;

if ((addr + data_len) % EEPROM_PAGE != 0) {

page_end += 1;

is_data_back = 1;

}



offset_current = 0;

    

/* 处理前面非页地址的数据,如果是页地址则不执行 */

for (unsigned int i = addr; i < (page_start * EEPROM_PAGE); i++) {

ret = eeprom_writebyte(i, data[offset_current]);

if (ret != 1) {

printf("%s, %s, %d: EepromWriteByte failed(%d)\n", __FILE__, __func__, __LINE__, ret);

return offset_current;

}

offset_current++;

}



/* 处理后续的数据,如果数据长度不足一页,则不执行 */

for (unsigned int page = page_start; page < page_end; page++) {

len = EEPROM_PAGE;

if ((page == (page_end - 1)) && (is_data_back)) {

len = (addr + data_len) % EEPROM_PAGE;






}

    

ret = eeprom_writepage(page * EEPROM_PAGE, &data[offset_current], len);

if (ret != len) {

printf("%s, %s, %d: EepromWritePage failed(%d)\n", __FILE__, __func__, __LINE__, ret);

return offset_current;

}

offset_current += EEPROM_PAGE;

}



return data_len;

}





5.4.3实验结果

程序编译烧写到开发板后,按下开发板的RESET按键,通过串口软件查看日志,具体内容如下: 

************ EEPROM Process ************

BlockSize = 0x8

Write Byte: 3 = !

Write Byte: 4 = "

Write Byte: 5 = #

Write Byte: 6 = $

Write Byte: 7 = %

Write Byte: 8 = &

Write Byte: 9 = '

Write Byte: 10 = (

Write Byte: 11 = )

Write Byte: 12 = *

Write Byte: 13 = +

Write Byte: 14 = ,

Write Byte: 15 = -

Write Byte: 16 = .

Write Byte: 17 = /

Write Byte: 18 = 0

Write Byte: 19 = 1

Write Byte: 20 = 2

Write Byte: 21 = 3

Write Byte: 22 = 4

Write Byte: 23 = 5

Write Byte: 24 = 6

Write Byte: 25 = 7

Write Byte: 26 = 8

Write Byte: 27 = 9

Write Byte: 28 = :

Write Byte: 29 = ;

Write Byte: 30 = <

Write Byte: 31 = =

Write Byte: 32 = >

Read Byte: 3 = !

Read Byte: 4 = "

Read Byte: 5 = #

Read Byte: 6 = $






Read Byte: 7 = %

Read Byte: 8 = &

Read Byte: 9 = '

Read Byte: 10 = (

Read Byte: 11 = )

Read Byte: 12 = *

Read Byte: 13 = +

Read Byte: 14 = ,

Read Byte: 15 = -

Read Byte: 16 = .

Read Byte: 17 = /

Read Byte: 18 = 0

Read Byte: 19 = 1

Read Byte: 20 = 2

Read Byte: 21 = 3

Read Byte: 22 = 4

Read Byte: 23 = 5

Read Byte: 24 = 6

Read Byte: 25 = 7

Read Byte: 26 = 8

Read Byte: 27 = 9

Read Byte: 28 = :

Read Byte: 29 = ;

Read Byte: 30 = <

Read Byte: 31 = =

Read Byte: 32 = >

...




5.5NFC碰一碰◆

NFC(Near Field Communication,近场通信)是由飞利浦公司发起,由诺基亚、索尼等著名厂商联合主推的一项无线技术。NFC由非接触式射频识别(Radio Frequency Identification,RFID)及互联互通技术整合演变而来,在单一芯片上结合感应式读卡器、感应式卡片和点对点的功能,能在短距离内与兼容设备进行识别和数据交换。这项技术最初只是RFID技术和网络技术的简单合并,现在已经演变成一种短距离无线通信技术,发展相当迅速。与RFID不同的是,NFC具有双向连接和识别的特点,工作于13.56MHz频率,作用距离为10cm左右。NFC技术在ISO 18092、ECMA 340和ETSI TS 102 190框架下推动标准化,同时也兼容应用广泛的ISO 14443 TYPEA、TYPEB以及Felica标准非接触式智能卡的基础架构。

使用NFC技术的设备(如智能手机)可以在彼此靠近的情况下进行数据交换,通过在单一芯片上集成感应式读卡器、感应式卡片和点对点通信,实现移动终端移动支付、门禁、移动身份识别等功能。

5.5.1硬件电路设计

硬件电路图如图5.5.1所示,NT3H1201是一款简单、低成本的NFC芯片,通过I2C接口和微控制器通信。芯片通过PCB上的射频天线从接触的有源NFC设备获取能量,并完成数据交互。交互的数据被写入片上的EEPROM,以便掉电后的再次读写。



图5.5.1硬件电路图




视频讲解

5.5.2程序设计

与以往设备配网技术相比,NFC“碰一碰”方案可以支持NFC功能的安卓手机和iOS 13.0以上系统的iPhone使用,从而为消费客户提供高效便捷的智慧生活无缝体验。

1.  主程序设计

如图5.5.2所示为NFC碰一碰主程序流程图,首先初始化I2C总线,然后控制I2C总线向NFC写入一段文本信息和一段网址信息,最后使用支持NFC功能的安卓手机或iOS 13.0以上系统的iPhone靠近小凌派RK2206开发板,就可以识别出一段文本信息和一个网址。



图5.5.2NFC碰一碰主程序流程图


程序代码如下: 

void nfc_process(void)

{

unsigned int ret = 0;



/* 初始化NFC设备 */

nfc_init();







ret = nfc_store_text(NDEFFirstPos, (uint8_t *)TEXT);

if (ret != 1) {

printf("NFC Write Text Failed: %d\n", ret);

}



ret = nfc_store_uri_http(NDEFLastPos, (uint8_t *)WEB);

if (ret != 1) {

printf("NFC Write Url Failed: %d\n", ret);

}



while (1) {

printf("==============NFC Example==============\r\n");

printf("Please use the mobile phone with NFC function close to the development board!\r\n");

printf("\n\n");

LOS_Msleep(1000);

}

}





2. NFC初始化程序设计

NFC碰一碰初始化主要包括I2C总线初始化。

/* NFC使用I2C的总线ID */

static unsigned int NFC_I2C_PORT = 2;



/* I2C配置 */

static I2cBusIo m_i2c2m0 =

{

.scl =  {.gpio = GPIO0_PD6, .func = MUX_FUNC1, .type = PULL_NONE, .drv = DRIVE_KEEP, .dir = LZGPIO_DIR_KEEP, .val = LZGPIO_LEVEL_KEEP},

.sda =  {.gpio = GPIO0_PD5, .func = MUX_FUNC1, .type = PULL_NONE, .drv = DRIVE_KEEP, .dir = LZGPIO_DIR_KEEP, .val = LZGPIO_LEVEL_KEEP},

.id = FUNC_ID_I2C2,

.mode = FUNC_MODE_M0,

};

/* I2C的时钟频率 */

static unsigned int m_i2c2_freq = 400000;



unsigned int NT3HI2cInit()

{

uint32_t *pGrf = (uint32_t *)0x41050000U;

uint32_t ulValue;



ulValue = pGrf[7];

ulValue &= ~((0x7 << 8) | (0x7 << 4));

ulValue |= ((0x1 << 8) | (0x1 << 4));

pGrf[7] = ulValue | (0xFFFF << 16);

printf("%s, %d: GRF_GPIO0D_IOMUX_H(0x%x) = 0x%x\n", __func__, __LINE__, &pGrf[7], pGrf[7]);



if (I2cIoInit(m_i2c2m0) != LZ_HARDWARE_SUCCESS)

{

printf("%s, %s, %d: I2cIoInit failed!\n", __FILE__, __func__, __LINE__);

return __LINE__;

}

if (LzI2cInit(NFC_I2C_PORT, m_i2c2_freq) != LZ_HARDWARE_SUCCESS)






{

printf("%s, %s, %d: LzI2cInit failed!\n", __FILE__, __func__, __LINE__);

return __LINE__;

}



return 0;

}



unsigned int nfc_init(void)

{

unsigned int ret = 0;

uint32_t *pGrf = (uint32_t *)0x41050000U;

uint32_t ulValue;



if (m_nfc_is_init == 1)

{

printf("%s, %s, %d: Nfc readly init!\n", __FILE__, __func__, __LINE__);

return __LINE__;

}



ret = NT3HI2cInit();

if (ret != 0)

{

printf("%s, %s, %d: NT3HI2cInit failed!\n", __FILE__, __func__, __LINE__);

return __LINE__;

}



m_nfc_is_init = 1;

return 0;

}







图5.5.3NDEF协议格式

3.  NFC写入数据程序设计

下面实现向NFC芯片写入NDEF数据包的程序。其中,NDEF数据包可包含多个Record信息段; 每个Record信息段可分为两大数据部分,分别为头部信息(即Header)和主体信息(即Payload,也就是传输信息内容); 头部信息又可分为3个数据部分,分别为标识符(即Identifier)、长度(即Record的大小信息)和类型,如图5.5.3所示。

ret = nfc_store_text(NDEFFirstPos, (uint8_t *)TEXT);

if (ret != 1) {

printf("NFC Write Text Failed: %d\n", ret);

}



ret = nfc_store_uri_http(NDEFLastPos, (uint8_t *)WEB);

if (ret != 1) {

printf("NFC Write Url Failed: %d\n", ret);

}




其中,nfc_store_text()和nfc_store_uri_http()两个函数首先按照rtdText.h和rtdUri.h中RTD协议进行处理,然后使用ndef.h中的NT3HwriteRecord()进行记录写入。

bool nfc_store_text(RecordPosEnu position, uint8_t *text)

{

NDEFDataStr data;



if (m_nfc_is_init == 0)

{

printf("%s, %s, %d: NFC is not init!\n", __FILE__, __func__, __LINE__);

return 0;

}



prepareText(&data, position, text);

return NT3HwriteRecord(&data);

}



bool nfc_store_uri_http(RecordPosEnu position, uint8_t *http)

{

NDEFDataStr data;



if (m_nfc_is_init == 0)

{

printf("%s, %s, %d: NFC is not init!\n", __FILE__, __func__, __LINE__);

return 0;

}



prepareUrihttp(&data, position, http);

return NT3HwriteRecord(&data);

}




NT3HwriteRecord()负责将需要下发的信息打包成NDEF协议报文,最后由I2C总线将NDEF协议报文发送给NFC设备。

bool NT3HwriteRecord(const NDEFDataStr *data)

{

uint8_t recordLength = 0, mbMe;

UncompletePageStr addPage;

addPage.page = 0;



// calculate the last used page

if (data->ndefPosition != NDEFFirstPos )

{

NT3HReadHeaderNfc(&recordLength, &mbMe);

addPage.page  = (recordLength + sizeof(NDEFHeaderStr) + 1) / NFC_PAGE_SIZE;



addPage.usedBytes = (recordLength + sizeof(NDEFHeaderStr) + 1) % NFC_PAGE_SIZE - 1;

}



int16_t payloadPtr = addFunct[data->ndefPosition](&addPage, data, data->ndefPosition);

if (payloadPtr == -1)

{

errNo = NT3HERROR_TYPE_NOT_SUPPORTED;

return false;

}








return writeUserPayload(payloadPtr, data, &addPage);

}




5.5.3实验结果

程序编译烧写到开发板后,按下开发板的RESET按键,通过串口软件查看日志,具体内容如下: 

==============NFC Example==============

Please use the mobile phone with NFC function close to the development board!

==============NFC Example==============

Please use the mobile phone with NFC function close to the development board!

...




5.6PWM控制◆

PWM(PulseWidth Modulation,脉冲宽度调制)是一种模拟信号电平数字编码的方法,将有效的电信号分散成离散形式,从而降低电信号所传递的平均功率。根据面积等效法则,改变脉冲的时间宽度,就可以等效获得所需要合成的相应幅值和频率的波形,实现模拟电路的数字化控制,从而降低系统的成本和功耗。许多微控制器和数字信号处理器内部都包含PWM控制逻辑单元,为数字化控制提供了方便。

RK2206芯片内部包含了3组PWM控制器,每组包含4个通道。



视频讲解

5.6.1硬件接口

PWM端口号对应GPIO引脚如表5.6.1所示,不同PWM对应不同的GPIO引脚输出。



表5.6.1PWM端口号对应GPIO引脚


端口号对应GPIO引脚端口号对应GPIO引脚
PWM0GPIO_B4PWM6GPIO_C3
PWM1GPIO_B5PWM7GPIO_C4
PWM2GPIO_B6PWM8GPIO_C5
PWM3GPIO_C0PWM9GPIO_C6
PWM4GPIO_C1PWM10GPIO_C7
PWM5GPIO_C2PWM11GPIO_D6


5.6.2程序设计

通过控制RK2206的PWM控制器,由小凌派RK2206开发板上的PWM端口输出PWM脉冲。



图5.6.1PWM控制主程序流程图

1.  主程序设计

如图5.6.1所示为PWM控制主程序流程图,开机LiteOS系统初始化后进入主程序。主程序首先创建一个PWM控制任务,用于操作控制PWM。接着任务采用循环的方式,控制一个PWM初始化使能和开启PWM,间隔5s后,停止PWM和PWM去使能。然后循环控制下一个PWM,从PWM0到PWM10依次循环。


初始化函数创建一个PWM控制任务。

unsigned int thread_id;

TSK_INIT_PARAM_S task = {0};

unsigned int ret = LOS_OK;



/*创建PWM控制任务*/

task.pfnTaskEntry = (TSK_ENTRY_FUNC)hal_pw_thread;

/*设置任务栈大小*/

task.uwStackSize = 2048;

/*设置任务名*/

task.pcName = "hal_pwm_thread";

/*设置任务优先级*/

task.usTaskPrio = 20;

ret = LOS_TaskCreate(&thread_id, &task);

if (ret != LOS_OK)

{

printf("Falied to create hal_pw_thread ret:0x%x\n", ret);

return;

}




2.  PWM控制程序设计

PWM控制程序主要包括PWM初始化使能、开启PWM、停止PWM和PWM去使能。

unsigned int ret;

/* PWM端口号对应于参考文件

device/rockchip/rk2206/adapter/hals/iot_hardware/wifiiot_lite/hal_iot_pwm.c

*/

unsigned int port = 0;



while (1)

{

/*PWM初始化*/

printf("===========================\n");

printf("PWM(%d) Init\n", port);

ret = IoTPwmInit(port);

if (ret != 0) {

printf("IoTPwmInit failed(%d)\n");

continue;

}



/*开启PWM*/

printf("PWM(%d) Start\n", port);

ret = IoTPwmStart(port, 50, 1000);

if (ret != 0) {

printf("IoTPwmStart failed(%d)\n");

continue;

}







/*延时5s*/

LOS_Msleep(5000);



/*停止PWM*/

printf("PWM(%d) end\n", port);

ret = IoTPwmStop(port);

if (ret != 0) {

printf("IoTPwmStop failed(%d)\n");

continue;

}



/*PWM去使能*/

ret = IoTPwmDeinit(port);

if (ret != 0) {

printf("IoTPwmInit failed(%d)\n");

continue;

}

printf("\n");



/*选择下一个PWM*/

port++;

if (port >= 11) {

port = 0;

}

}






5.6.3实验结果

程序编译烧写到开发板后,按下开发板的RESET按键,通过串口软件查看日志,任务每隔5s控制不同的PWM输出,从PWM0到PWM10依次循环输出,具体内容如下: 

===========================

[HAL INFO] setting GPIO0-12 to 1

[HAL INFO] setting route 41050204 = 00100010, 00000010

[HAL INFO] setting route for GPIO0-12

[HAL INFO] setting GPIO0-12 pull to 2

[GPIO:D]LzGpioInit: id 12 is initialized successfully

[HAL INFO] setting GPIO0-12 to 1

[HAL INFO] setting route 41050204 = 00100010, 00000010

[HAL INFO] setting route for GPIO0-12

[HAL INFO] setting GPIO0-12 pull to 2

[HAL INFO] PINCTRL Write before set reg val=0x10

[HAL INFO] PINCTRL Write after  set reg val=0x10

PWM(0) start

[HAL INFO] channel=0, period_ns=1000000, duty_ns=500000

[HAL INFO] channel=0, period=40000, duty=20000, polarity=0

[HAL INFO] Enable channel=0

IotProcess: sleep 5 sec!

PWM(0) end

[HAL INFO] Disable channel=0

===========================

[HAL INFO] setting GPIO0-13 to 1

[HAL INFO] setting route 41050204 = 00200020, 00000030

[HAL INFO] setting route for GPIO0-13

[HAL INFO] setting GPIO0-13 pull to 2






[GPIO:D]LzGpioInit: id 13 is initialized successfully

[HAL INFO] setting GPIO0-13 to 1

[HAL INFO] setting route 41050204 = 00200020, 00000030

[HAL INFO] setting route for GPIO0-13

[HAL INFO] setting GPIO0-13 pull to 2

[HAL INFO] PINCTRL Write before set reg val=0x30

[HAL INFO] PINCTRL Write after  set reg val=0x30

PWM(1) start

[HAL INFO] channel=1, period_ns=1000000, duty_ns=500000

[HAL INFO] channel=1, period=40000, duty=20000, polarity=0

[HAL INFO] Enable channel=1

IotProcess: sleep 5 sec!

PWM(1) end

[HAL INFO] Disable channel=1

...




5.7看门狗◆

看门狗定时器(WatchDog Timer,WDT)用于监视和控制微控制器的运行状态,确保微控制器系统能够稳定、可靠地运行。看门狗的主要作用是防止程序发生死循环或系统崩溃,通过定期检查微处理器内部的情况,一旦发生错误或异常,就向微处理器发出重启信号,从而避免系统陷入停滞状态或发生不可预料的后果。

看门狗本质是一个定时器电路,基于一个输入和一个输出,其中输入称为“喂狗”,通过外部输入重装载看门狗计数器的值,而输出连接到另外一个部分的复位端。当微控制器正常运行时,会定期通过喂狗操作给看门狗定时器清零,防止看门狗超时而发出复位信号。如果微控制器运行异常,未能按时进行喂狗操作,看门狗定时器达到设定值后,就会给微控制器发出复位信号,使其复位,从而恢复正常的运行状态。看门狗命令在程序的中断中拥有最高的优先级。



视频讲解

5.7.1硬件看门狗工作原理

RK2206芯片内置了看门狗电路硬件电路,其工作原理如图5.7.1所示。看门狗电路包含看门狗输入时钟、递减计数器、“喂狗”输入和复位输出; 看门狗输入时钟驱动递减计数器工作,当递减计数器为0时,看门狗超时触发复位信号,重启CPU; 如果CPU进行“喂狗”,即重置计数器,递减计数器复位,重新开始递减计数。



图5.7.1硬件看门狗工作原理


5.7.2程序设计

通过控制RK2206的看门狗控制器,实现小凌派RK2206开发板看门狗功能。

1.  主程序设计

如图5.7.2所示为看门狗主程序流程图,开机LiteOS系统初始化后进入主程序。主程序首先创建一个看门狗任务,用于控制看门狗。任务启动先初始化看门狗,并设置看门狗超时时间。接着任务采用循环的方式,间隔1s喂狗一次,10s后不再喂狗,然后等待看门狗超时重启系统。



图5.7.2看门狗主程序流程图


初始化函数创建一个看门狗任务。

unsigned int thread_id;

TSK_INIT_PARAM_S task = {0};

unsigned int ret = LOS_OK;



/*创建看门狗任务*/

task.pfnTaskEntry = (TSK_ENTRY_FUNC)watchdog_process;

/*设置任务栈大小*/

task.uwStackSize = 20480;







/*设置任务名*/

task.pcName = "watchdog process";

/*设置任务优先级*/

task.usTaskPrio = 24;

ret = LOS_TaskCreate(&thread_id, &task);

if (ret != LOS_OK)

{

printf("Falied to create task ret:0x%x\n", ret);

return;

}






2.  看门狗控制程序设计

看门狗控制程序主要包括看门狗初始化、设置看门狗超时时间和喂狗操作。

uint32_t current = 0;



/*初始化看门狗*/

printf("%s: start\n", __func__);

LzWatchdogInit();

/* 设置看门狗超时时间,实际是1.3981013 * (2 ^ 4) = 22.3696208s */

LzWatchdogSetTimeout(20);

/*启动看门狗*/

LzWatchdogStart(LZ_WATCHDOG_REBOOT_MODE_FIRST);



while (1)

{

printf("Wathdog: current(%d)\n", ++current);

if (current <= 10)

{

/*喂狗操作*/

printf("    freedog\n");

LzWatchdogKeepAlive();

}

else

{

/*不喂狗操作*/

printf("    not freedog\n");

}

/*延时1s*/

LOS_Msleep(1000);

}




5.7.3实验结果

程序编译烧写到开发板后,按下开发板的RESET按键,通过串口软件查看日志,看门狗任务每隔1s喂狗一次,10s后不再喂狗; 当超过看门狗超时时间,小凌派RK2206开发板系统重启,具体内容如下: 

watchdog_process: start

Wathdog: current(1)

freedog

Wathdog: current(2)

freedog

Wathdog: current(3)

freedog







Wathdog: current(4)

freedog

Wathdog: current(5)

freedog

Wathdog: current(6)

freedog

Wathdog: current(7)

freedog

Wathdog: current(8)

freedog

Wathdog: current(9)

freedog

Wathdog: current(10)

freedog

Wathdog: current(11)

not freedog

Wathdog: current(12)

not freedog

Wathdog: current(13)

not freedog

Wathdog: current(14)

not freedog

Wathdog: current(15)

not freedog

Wathdog: current(16)

not freedog

Wathdog: current(17)

not freedog

Wathdog: current(18)

not freedog

Wathdog: current(19)

not freedog

Wathdog: current(20)

not freedog

Wathdog: current(21)

not freedog

Wathdog: current(22)

not freedog

Wathdog: current(23)

not freedog

Wathdog: current(24)

not freedog

Wathdog: current(25)

not freedog

Wathdog: current(26)

not freedog

Wathdog: current(27)

not freedog

Wathdog: current(28)

not freedog

Wathdog: current(29)

not freedog

Wathdog: current(30)

not freedog

Wathdog: current(31)

not freedog

Wathdog: current(32)






not freedog

entering kernel init...

hilog will init.

[IOT:D]IotInit: start ....

[MAIN:D]Main: LOS_Start ...

Entering scheduler

[IOT:D]IotProcess: start ....




5.8思考和练习◆

(1) OpenHarmony系统提供了哪些外设的接口?

(2) OpenHarmony系统如何使用GPIO接口功能?

(3) OpenHarmony系统如何使用I2C接口功能?

(4) OpenHarmony系统如何使用PWM接口功能?

(5) OpenHarmony系统如何使用SPI接口功能?

(6) 设计并编写一个程序,实现如下功能: 

从EEPROM中读取事先存储的数据,并将读取的数据实时显示到LCD上。

(7) 设计并编写一个程序,实现如下功能: 

按键控制PWM通道输出; 按键K1用于选择PWM通道,按键K2用于选择PWM频率,按键K3用于选择PWM占空比,按键K4用于恢复默认PWM配置。

(8) 设计并编写一个程序,实现如下功能: 

使用LED灯指示系统运行状态,当看门狗喂狗时,LED灯闪烁表示正在喂狗。