标签:
catalogue
1. 基于标准外设库的软件开发 2. 基于固件库实现串口输出(发送)程序 3. 红外接收实验
1. 基于标准外设库的软件开发
0x1: STM32标准外设库概述
STM32标准外设库之前的版本也称固件函数库或简称固件库(即操作片外固件的代码集合),是一个固件函数包,它由程序、数据结构和宏组成,包括了微控制器所有外设的性能特征。该函数库还包括每一个外设的驱动描述和应用实例,为开发者访问底层硬件提供了一个中间API,通过使用固件函数库,无需深入掌握底层硬件细节,开发者就可以轻松应用每一个外设。因此,使用固态函数库可以大大减少用户的程序编写时间,进而降低开发成本。每个外设驱动都由一组函数组成,这组函数覆盖了该外设所有功能。每个器件的开发都由一个通用API (application programming interface 应用编程界面)驱动,API对该驱动程序的结构,函数和参数名称都进行了标准化,顺便提一句,arduino之所以入门容易开发简单就是因为我们很多时候是要"面向固件库编程",很多复杂的外设操作都通过简单的API调用就完成了
ST公司2007年10月发布了V1.0版本的固件库,MDK ARM3.22之前的版本均支持该库。2008年6月发布了V2.0版的固件库,从2008年9月推出的MDK ARM3.23版本至今均使用V2.0版本的固件库。V3.0以后的版本相对之前的版本改动较大
0x2: 使用标准外设库开发的优势
使用标准外设库进行开发最大的优势就在于可以使开发者不用深入了解底层硬件细节就可以灵活规范的使用每一个外设。标准外设库覆盖了从GPIO到定时器,再到CAN、I2C、SPI、UART和ADC等等的所有标准外设。对应的C源代码只是用了最基本的C编程的知识,所有代码经过严格测试,易于理解和使用,并且配有完整的文档,非常方便进行二次开发和应用
0x3: STM32F10XXX标准外设库结构与文件描述
表 5?4 STM32F10XXX V3.4标准外设库文件夹描述
STM32F10x_StdPeriph_Lib_V3.4.0 |
_htmresc |
本文件夹包含了所有的html页面资源 |
Libraries |
CMSIS微控制器软件接口标准(CMSIS:Cortex Microcontroller Software Interface Standard) |
是 Cortex-M 处理器系列的与供应商无关的软件抽象层。 使用CMSIS,可以为处理器和外设实现一致且简单的软件接口,从而简化软件的重用 |
STM32F10x_StdPeriph_Driver |
inc |
标准外设库驱动头文件 |
src |
标准外设库驱动源文件 |
|
Project |
Examples |
标准外设库驱动的完整例程 |
Template |
MDK-ARM |
KEIL RVMDK的项目模板示例 |
RIDE |
Raisonance RIDE的项目模板示例 |
|
EWARM |
IAR EWARM的项目模板示例 |
|
Utilities |
STM3210-EVAL |
本文件夹包含了用于STM3210B-EVAL和STM3210E-EVAL评估板的专用驱动 |
标准外设库的第一部分是CMSIS 和STM32F10x_StdPeriph_Driver,CMSIS 是独立于供应商的Cortex-M 处理器系列硬件抽象层,为芯片厂商和中间件供应商提供了简单的处理器软件接口,简化了软件复用工作,降低了Cortex-M 上操作系统的移植难度,并减少了新入门的微控制器开发者的学习曲线和新产品的上市时间。STM32F10x_StdPeriph_Driver则包括了分别对应包括了所有外设对应驱动函数,这些驱动函数均使用C语言编写,并提供了统一的易于调用的函数接口,供开发者使用。Project文件夹中则包括了ST官方的所有例程和基于不同编译器的项目模板,这些例程是学习和使用STM32的重要参考。Utilities包含了相关评估板的示例程序和驱动函数,供使用官方评估板的开发者使用,很多驱动函数同样可以作为学习的重要参考
0x4: STM32F10xxx标准外设库体系结构
文件名 |
功能描述 |
具体功能说明 |
core_cm3.h core_cm3.c |
Cortex-M3内核及其设备文件 |
访问Cortex-M3内核及其设备:NVIC,SysTick等 访问Cortex-M3的CPU寄存器和内核外设的函数 |
stm32f10x.h |
微控制器专用头文件 |
这个文件包含了STM32F10x全系列所有外设寄存器的定义(寄存器的基地址和布局)、位定义、中断向量表、存储空间的地址映射等 |
system_stm32f10x.h system_stm32f10x.c |
微控制器专用系统文件 |
函数SystemInit,用来初始化微控制器 函数Sysem_ExtMemCtl,用来配置外部存储器控制器。它位于文件startup_stm32f10x_xx.s /.c,在跳转到main前调用 SystemFrequncy,该值代表系统时钟频率 |
startup_stm32f10x_Xd.s |
编译器启动代码 |
微控制器专用的中断处理程序列表(与头文件一致) 弱定义(Weak)的中断处理程序默认函数(可以被用户代码覆盖) 该文件是与编译器相关的 |
stm32f10x_conf.h |
固件库配置文件 |
通过更改包含的外设头文件来选择固件库所使用的外设,在新建程序和进行功能变更之前应当首先修改对应的配置。 |
stm32f10x_it.h stm32f10x_it.c |
外设中断函数文件 |
用户可以相应的加入自己的中断程序的代码,对于指向同一个中断向量的多个不同中断请求,用户可以通过判断外设的中断标志位来确定准确的中断源,执行相应的中断服务函数。 |
stm32f10x_ppp.h stm32f10x_ppp.c |
外设驱动函数文件 |
包括了相关外设的初始化配置和部分功能应用函数,这部分是进行编程功能实现的重要组成部分。 |
Application.c |
用户文件 |
用户程序文件,通过标准外设库提供的接口进行相应的外设配置和功能设计。 |
0x5: 基于CMSIS标准的软件架构
对于ARM公司来说,一个ARM内核往往会授权给多个厂家,生产种类繁多的产品,如果没有一个通用的软件接口标准,那么当开发者在使用不同厂家的芯片时将极大的增加了软件开发成本,因此,ARM与Atmel、IAR、Keil、hami-nary Micro、Micrium、NXP、SEGGER和ST等诸多芯片和软件厂商合作,将所有Cortex芯片厂商产品的软件接口标准化,制定了CMSIS标准。此举意在降低软件开发成本,尤其针对新设备项目开发,或者将已有软件移植到其他芯片厂商提供的基于Cortex处理器的微控制器的情况。有了该标准,芯片厂商就能够将他们的资源专注于产品外设特性的差异化,并且消除对微控制器进行编程时需要维持的不同的、互相不兼容的标准的需求,从而达到降低开发成本的目的
基于CMSIS标准的软件架构(或者叫固件库架构)主要分为以下4层
1. 用户应用层 2. 操作系统及中间件接口层 3. CMSIS层: CMSIS层起着承上启下的作用 1) 一方面该层对硬件寄存器层进行统一实现,屏蔽了不同厂商对Cortex-M系列微处理器核内外设寄存器的不同定义 2) 另一方面又向上层的操作系统及中间件接口层和应用层提供接口,简化了应用程序开发难度,使开发人员能够在完全透明的情况下进行应用程序开发。也正是如此,CMSIS层的实现相对复杂 4. 硬件寄存器层
0x6: 使用方式
在实际开发过程中,根据应用程序的需要,可以采取2种方法使用标准外设库(StdPeriph_Lib)
1. 使用外设驱动: 这时应用程序开发基于外设驱动的API(应用编程接口)。用户只需要配置文件"stm32f10x_conf.h",并使用相应的文件例如"stm32f10x_ppp.h/.c"即可 2. 不使用外设驱动: 这时应用程序开发基于外设的寄存器结构和位定义文件(需要了解单片机的大量硬件、引脚细节)
标准外设库(StdPeriph_Lib)支持STM32F10xxx系列全部成员:大容量,中容量和小容量产品,实际开发中根据使用的STM32产品具体型号,用户可以通过文件"stm32f10x.h"中的预处理define或者通过开发环境中的全局设置来配置标准外设库(StdPeriph_Lib),一个define对应一个产品系列
STM32F10x_LD:STM32小容量产品
STM32F10x_MD:STM32中容量产品
STM32F10x_HD:STM32大容量产品
在库文件中这些define的具体作用范围是
1. 文件"stm3210f.h"中的中断IRQ定义 2. 启动文件中的向量表,小容量,中容量,大容量产品各有一个启动文件 3. 外设存储器映像和寄存器物理地址 4. 产品设置: 外部晶振(HSE)的值等 5. 系统配置函数
因此通过宏定义这种方式,可以使标准外设库适用于不同系列的产品,同时也方便与不同产品之间的软件移植,极大的方便了软件的开发
0x7: 命名规范
标准外设库中的主要外设均采用了缩写的形式,通过这些缩写可以很容易的辨认对应的外设。
缩写 |
外设/单元 |
ADC |
模数转换器 |
BKP |
备份寄存器 |
CAN |
控制器局域网模块 |
CEC |
|
CRC |
CRC计算单元 |
DAC |
数模转换器 |
DBGMCU |
调试支持 |
DMA |
直接内存存取控制器 |
EXTI |
外部中断事件控制器 |
FLASH |
闪存存储器 |
FSMC |
灵活的静态存储器控制器 |
GPIO |
通用输入输出 |
I2C |
I2C接口 |
IWDG |
独立看门狗 |
PWR |
电源/功耗控制 |
RCC |
复位与时钟控制器 |
RTC |
实时时钟 |
SDIO |
SDIO接口 |
SPI |
串行外设接口 |
TIM |
定时器 |
USART |
通用同步/异步收发器 |
WWDG |
窗口看门狗 |
标准外设库遵从以下命名规则 PPP表示任一外设缩写,例如:ADC。源程序文件和头文件命名都以“stm32f10x_”作为开头,例如:stm32f10x_conf.h
外设函数的命名以该外设的缩写加下划线为开头。每个单词的第一个字母都由英文字母大写书写,例如:SPI_SendData。在函数名中,只允许存在一个下划线,用以分隔外设缩写和函数名的其它部分。对于函数命名
Relevant Link:
http://baike.baidu.com/link?url=X3kL65ER2yug2m-_XhgXTggAd7uH7VCnyhdaJ2ddxYt-Nj8oqB46tWDhNngqyrPnuAzzs8wJe56NzIJi-6zWWa http://www.cnblogs.com/emouse/archive/2011/11/29/2268441.html
2. 基于固件库实现串口输出(发送)程序
0x1: 关键库函数
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data) 发送函数,它告诉我们很重要的一点,那就是串口是以"位"来传输的 FlagStatus USART_GetFlagStatus(USART_TypeDef* USARTx, uint16_t USART_FLAG) 用它来得知串口的状态
0x2: 参考printf函数编写串口发送函数(自定义一个完全脱库标准C库的新函数)
#include <stdio.h> #include <stdarg.h> /************************************************* * Function Name : USART1_printf * Description : * Input : * Output : NONE * Return : NONE *************************************************/ void USART1_printf (char *fmt, ...) { char buffer[CMD_BUFFER_LEN+1]; u8 i = 0; va_list arg_ptr; va_start(arg_ptr, fmt); vsnprintf(buffer, CMD_BUFFER_LEN+1, fmt, arg_ptr); while ((i < CMD_BUFFER_LEN) && buffer[i]) { USART_SendData(USART1, (u8) buffer[i++]); while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET); } va_end(arg_ptr); } /************************************************* * Function Name : USART1_SendData * Description : 串口1发送 * Input : char *Buffer * Output : NONE * Return : NONE *************************************************/ void USART1_SendData(char *Buffer) { u8 Counter = 0; while( (Counter == 0) || (Buffer[Counter] != 0) ) //条件... { USART_SendData(USART1, Buffer[Counter++]); while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET); } }
0x3: Code
/*---------------------------------------------------------------------------- * Name: Hello.c * Purpose: Hello World Example * Note(s): *---------------------------------------------------------------------------- * This file is part of the uVision/ARM development tools. * This software may only be used under the terms of a valid, current, * end user licence from KEIL for a compatible version of KEIL software * development tools. Nothing else gives you the right to use this software. * * This software is supplied "AS IS" without warranties of any kind. * * Copyright (c) 2012 Keil - An ARM Company. All rights reserved. *----------------------------------------------------------------------------*/ #include <stdio.h> /* prototype declarations for I/O functions */ #include <stm32f10x.h> /* STM32F10x definitions */ #include <stm32f10x_usart.h> #include <stdarg.h> #include <string.h> #define CMD_BUFFER_LEN 64u static void delayms(int cnt){ int i; while(cnt--) for (i=0; i<7333; i++); } void USART2_printf (char *fmt, ...) { char buffer[CMD_BUFFER_LEN+1] = {0}; u8 i = 0; int len = 0; va_list arg_ptr; memset(buffer, ‘\x00‘, CMD_BUFFER_LEN+1); len = strlen(fmt) + 1; va_start(arg_ptr, fmt); vsnprintf(buffer, len, fmt, arg_ptr); while ((i < len) && buffer[i] != ‘\x00‘) { USART_SendData(USART1, (u8) buffer[i++]); while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET) ; } va_end(arg_ptr); } extern void SER_Init(void); /* see Serial.c */ /*---------------------------------------------------------------------------- main program *----------------------------------------------------------------------------*/ int main (void) { /* execution starts here */ SER_Init (); /* initialize the serial interface */ USART2_printf ("Hello World\n"); /* the ‘printf‘ function call */ while (1) { /* An embedded program does not stop and */ USART2_printf ("Hello World\n"); /* ... */ /* never returns. We use an endless loop. */ delayms(3000); } /* Replace the dots (...) with your own code.*/ }
使用64字节的缓冲数组保存需要发送的数据,然后通过while循环逐byte发送往Terminal终端
为了显示方便,还可以加上sleep函数
static void delayms(int cnt){ int i; while(cnt--) for (i=0; i<7333; i++); }
需要明白的,我们从指令层面看while循环,由于指令周期、机器周期都是可时间量化的,因此可以用while来实现spin CPU空转,即sleep
Relevant Link:
http://www.cnblogs.com/mocet/p/stm32f10x_usart_InputOutout_Function_Design_1.html
3. 红外接收实验
二进制脉冲码的形式有多种,其中最为常见的是PWM码(脉冲宽度调制码)和PPM码(脉冲位置调制码,脉冲串之间的时间间隔来实现信号调制),如果要开发红外接收设备,一定要知道红外发射器(例如遥控器)的编码方式和载波频率,我们才可以选取一体化红外接收头和制定解码方案
/********************************************************************************** * ***********************************************************************************/ #include "stm32f10x.h" #include "stm32f10x_exti.h" //#include "stm8s_beep.h" #include "stm32f10x_systick.h" #define LED1_0 GPIOD->BRR = 0x00000100 #define LED2_0 GPIOD->BRR = 0x00000200 #define LED3_0 GPIOD->BRR = 0x00000400 #define LED4_0 GPIOD->BRR = 0x00000800 #define LED1_1 GPIOD->BSRR = 0x00000100 #define LED2_1 GPIOD->BSRR = 0x00000200 #define LED3_1 GPIOD->BSRR = 0x00000400 #define LED4_1 GPIOD->BSRR = 0x00000800 #define IR_Hongwai_0 GPIOE->BRR = 0x00000004 //??????? #define IR_Hongwai_1 GPIOE->BSRR = 0x00000004 //??????? #define IR_Hongwai_x GPIO_ReadInputDataBit(GPIOE, GPIO_Pin_2) //???????? unsigned int TimeByte; volatile unsigned int IR_Tireafg[4] = {0, 0, 0, 0}; //unsigned int IR_xidwrit[8] = {0, 0, 0, 0, 0, 0 ,0, 0}; /* * GPIO??????? */ void GPIO_InitStructReadtempCmd(void) { GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_2; //??GPIO?? GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; //????????? GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; //??????50MHZ GPIO_Init(GPIOE, &GPIO_InitStruct); //??????? GPIO_InitStruct.GPIO_Pin = GPIO_Pin_5; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; //?????????? GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStruct); GPIO_InitStruct.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10 | GPIO_Pin_11; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOD, &GPIO_InitStruct); } /* * ????????? */ void RCC_APB2PeriphReadtempyCmd(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); //??GPIOB???? RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE, ENABLE); //??GPIOE???? RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE); //??GPIOD???? RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO , ENABLE); //??AFIO???????? } /* * ?????????Count1 * 10us */ unsigned int IR_HongwaiRead_LSB_Cmd() { unsigned int Count1 = 0; //?????? IR_Hongwai_0; //??????? do //????? { Count1++; //?????1 Delay_10us(1); //??10us } while(IR_Hongwai_x == 0); //??????????????????????? return(Count1); //???????? } /* * ?????????Count2 * 10us */ unsigned int IR_HongwaiRead_MSB_Cmd() { unsigned int Count2 = 0; //?????? IR_Hongwai_1; //??????? do //????? { Count2++; //?????1 Delay_10us(1); //??10us } while(IR_Hongwai_x == 1); //??????????????????????? return(Count2); } /* * ???? */ int main(void) { SystemInit(); //?????????72M?? SYSTICK_InitStructReadTCmd(); //???SysTick?????? RCC_APB2PeriphReadtempyCmd(); //???????????? GPIO_InitStructReadtempCmd(); //???GPIO??????? EXTI_InitStructReadtempCmd(); //???EXTI??????? NVIC_InitStructReadtempCmd(); //???NVIC??????? while(1) { } } /* * EXTI????????? */ void EXTI2_IRQHandler(void) { unsigned char i = 0; unsigned int Countline2 = 0; IR_Hongwai_1; Countline2 = IR_HongwaiRead_LSB_Cmd(); //?????? 9ms?? if((Countline2 < 286) || (Countline2 > 305)) //??8694us ??9272us ???????? { return; } Countline2 = IR_HongwaiRead_MSB_Cmd(); //?????? 4.5ms?? if((Countline2 < 138) || (Countline2 > 155)) //??4195us ??4712us ???????? { return; } TimeByte = 0; for(i = 1; i < 14; i++) { TimeByte = TimeByte >> 1; Countline2 = IR_HongwaiRead_LSB_Cmd(); //?????0.56 ?? if((Countline2 < 14) || (Countline2 > 22)) //??425us ??851us ???????? { return; } Countline2 = IR_HongwaiRead_MSB_Cmd(); //?????0.56?? if((Countline2 < 14) || (Countline2 > 65)) //??425us ??1793us ???????? { return; } if( Countline2 > 50) //???????1300us?1???0 { TimeByte |= 0x80; //?1 } } IR_Tireafg[0] = TimeByte; TimeByte = 0; for(i = 14; i < 27; i++) { TimeByte = TimeByte >> 1; Countline2 = IR_HongwaiRead_LSB_Cmd(); if((Countline2 < 14) || (Countline2 > 22)) { return; } Countline2 = IR_HongwaiRead_MSB_Cmd(); if((Countline2 < 14) || (Countline2 > 65)) { return; } if( Countline2 > 50) { TimeByte |= 0x80; } } IR_Tireafg[1] = TimeByte; TimeByte = 0; for(i = 27; i < 35; i++) { TimeByte = TimeByte >> 1; Countline2 = IR_HongwaiRead_LSB_Cmd(); if((Countline2 < 14) || (Countline2 > 22)) { return; } Countline2 = IR_HongwaiRead_MSB_Cmd(); if((Countline2 < 14) && (Countline2 > 65)) { return; } if( Countline2 > 50) { TimeByte |= 0x80; } } IR_Tireafg[2] = TimeByte; TimeByte = 0; for(i = 35; i < 43; i++) { TimeByte = TimeByte >> 1; Countline2 = IR_HongwaiRead_LSB_Cmd(); if((Countline2 < 14) || (Countline2 > 22)) { return; } Countline2 = IR_HongwaiRead_MSB_Cmd(); if((Countline2 < 14) || (Countline2 > 65)) { return; } if( Countline2 > 52) { TimeByte |= 0x80; } } IR_Tireafg[3] = TimeByte; //************************?????????***************************************// /* if(IR_Tireafg[0]!= 0x11A) { return; } */ //************************?????????***************************************// /* do { if(IR_Tireafg[2] == ~IR_Tireafg[3]) { flag = 0; } } while(flag == 1); */ /* if(IR_Tireafg[2] != ~IR_Tireafg[3]) { flag = 0; } */ //************************??????LED??**************************************// switch (IR_Tireafg[2]) { case 0x00: //?? 0 LED1_1; LED2_0; LED3_0; LED4_0; break; case 0x01: //?? 1 LED1_0; LED2_1; LED3_0; LED4_0; break; case 0x02: //?? 2 LED1_0; LED2_0; LED3_1; LED4_0; break; case 0x03: //?? 3 LED1_0; LED2_0; LED3_0; LED4_1; break; case 0x04: //?? 4 LED1_0; LED2_0; LED3_1; LED4_0; break; case 0x05: //?? 5 LED1_0; LED2_1; LED3_0; LED4_0; break; case 0x06: //?? 6 LED1_1; LED2_0; LED3_0; LED4_0; break; case 0x07: //?? 7 LED1_1; LED2_0; LED3_1; LED4_0; break; case 0x08: //?? 8 LED1_1; LED2_0; LED3_0; LED4_1; break; case 0x09: //?? 9 LED1_0; LED2_1; LED3_0; LED4_1; break; case 0x15: //??? LED1_0; LED2_1; LED3_1; LED4_0; break; case 0x1C: //??? LED1_0; LED2_0; LED3_0; LED4_0; break; case 0x14: //OSD? LED1_1; LED2_1; LED3_0; LED4_0; break; case 0x0E: //RECALL? LED1_0; LED2_0; LED3_1; LED4_1; break; case 0x19: //SLEEP? LED1_1; LED2_1; LED3_1; LED4_0; break; case 0x0A: //A/C? LED1_0; LED2_1; LED3_1; LED4_1; break; case 0x0F: //TV/AV? LED1_1; LED2_1; LED3_1; LED4_1; break; case 0x13: //PP? LED1_1; LED2_0; LED3_1; LED4_0; break; case 0x0C: //GAME? LED1_0; LED2_1; LED3_1; LED4_1; break; case 0x1E: //V-? LED1_1; LED2_1; LED3_1; LED4_0; break; case 0x1F: //V+? LED1_0; LED2_0; LED3_1; LED4_0; break; case 0x1B: //P+? LED1_0; LED2_0; LED3_0; LED4_1; break; case 0x1A: //P-? LED1_1; LED2_0; LED3_0; LED4_0; break; case 0x10: //MENU? LED1_0; LED2_1; LED3_0; LED4_0; break; default : break; } // Beep_lookCmd(); //?????? EXTI_ClearITPendingBit(EXTI_Line2); //??EXTI2??????? }
Relevant Link:
http://www.iqiyi.com/w_19rrdz9g91.html#curid=5449021609_7b2174ee370808596288e2209eef0b75 http://bbs.21ic.com/icview-243059-1-1.html http://wenku.baidu.com/view/2d0b4636a32d7375a417802e.html http://blog.csdn.net/houqi02/article/details/51585551 http://bbs.21ic.com/icview-649262-1-1.html http://wenku.baidu.com/link?url=F4r-R2rp-cF8lw7zSxuLYVWRoLdXeCQYt2Kf4hO9Kb7JMe1n7eOxkY-5t4Ar3990U5EmoungBQCyGFJitjsFqSSId5joGVgND6gQntg0ipO
Copyright (c) 2016 LittleHann All rights reserved
标签:
原文地址:http://www.cnblogs.com/LittleHann/p/5814316.html