第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芯片采用一种逐次逼近寄存器型模数转换器(SuccessiveApproximation 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色图像的TFTLCD,包含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(ElectricallyErasable Programmable ReadOnly 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 TYPEA、TYPEB以及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(PulseWidth 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灯闪烁表示正在喂狗。