IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 嵌入式 -> STM32F103使用DHT11采集温湿度通过软件I2C驱动OLED显示 -> 正文阅读

[嵌入式]STM32F103使用DHT11采集温湿度通过软件I2C驱动OLED显示

一.温湿度传感器介绍


在这里插入图片描述
1.主要参数
供电电压:3.3 - 5.5V直流电
输出为单总线数字信号
温度测量范围0-50度(精度正负2度,分辨率1度)
湿度测量范围为20-90%RH(精度为正负5%,分辨率1%)

2.硬件连接
模块的VCC 接单片机的5V
模块的GND 接单片机的GND
模块的DAT 接单片机任意引脚
vcc和gnd之间可以加一个电容,用于去耦滤波

3.温湿度采集原理
采用单总线双向串行通信协议,每次采集都要由单片机发起开始信号,然后DHT11会向单片机发送响应并开始传输40位数据帧,高位在前。

(1)数据格式为:

第一二个字节: 8bit湿度整数数据+8bit湿度小数数据
第三四个字节: 8bit温度整数数据+8bit温度小数数据
第五个字节 : 8bit校验位(它是前四个数据相加后八位的数值)
温湿度小数部分默认为0,即单片机采集的数据都是整数,校验位为4个字节的数据相加取结果的低8位数据作为校验和;

示例:
0011 0101 0000 0000 0001 1000 0000 0000 0100 1001
湿度高八位 湿度低八位 温度高八位 温度低八位 检验位
计算 : 0011 0101 + 0001 1000
结果: 0100 1101 若不等于 01001101 ,则本次接收数据不正确,重新接收数据

(2)温湿度传感器时序介绍
总线空闲状态为高电平,主机把总线拉低等待DHT11响应,主机把总线拉低必须大于18毫秒,保证DHT11能检测起始信号。DHT11接收到主机的开始信号后,等待主机开始信号结束,然后发送80us低电平响应信号,主机发送开始信号结束后,延时等待20

二.代码示例


DHT11.h头文件

#ifndef __DHT11_H
#define __DHT11_H 
#include "stm32f10x.h"  
 
//IO方向设置
#define DHT11_IO_IN()  {GPIOB->CRL&=0XFFFFFFF0;GPIOB->CRL|=0x08;}//上拉/下拉输入模式
#define DHT11_IO_OUT() {GPIOB->CRL&=0XFFFFFFF0;GPIOB->CRL|=0x03;}//推挽输出模式,50MHZ
IO操作函数											   
#define	DHT11_DQ_OUT PBout(0) //数据端口	PB0 
#define	DHT11_DQ_IN  PBin(0)  //数据端口	PB0 

u8 DHT11_Init(void);//初始化DHT11
u8 DHT11_Read_Data(void);//读取温湿度
u8 DHT11_Read_Byte(void);//读出一个字节
u8 DHT11_Read_Bit(void);//读出一个位
u8 DHT11_Check(void);//检测是否存在DHT11
void DHT11_Rst(void);//复位DHT11    
#endif

DHT11.c文件

#include "dht11.h"
#include "delay.h"
#include "oled.h"
    
//起始信号
void DHT11_Rst(void)	   
{                 
	  DHT11_IO_OUT(); 	//设置推挽输出模式,50MHZ
    DHT11_DQ_OUT=0; 	//拉低电平,主机开始发送起始信号
    delay_ms(20);    	//至少18ms,保证从机检测到起始信号
	                    //从机检测到后,从机等待主机起始信号 结束-->>拉高电平	
    DHT11_DQ_OUT=1; 	//主机拉高电平,起始信号结束,延时等待,准备接收回响信号
	  delay_us(30);     //主机拉高20~40us
}

//主机接收起始信号,等待从机DHT11的回应
//返回1:未检测到DHT11的存在
//返回0:存在
u8 DHT11_Check(void) 	   
{   
	u8 retry=0;
	DHT11_IO_IN();//设置上拉/下拉输入模式
  while (DHT11_DQ_IN&&retry<100)//DHT11会拉低40~80us
	{
		retry++;
		delay_us(1);
	};	 
	if(retry>=100)return 1;
	else retry=0;
  while (!DHT11_DQ_IN&&retry<100)//DHT11拉低后会再次拉高40~80us,准备输出,只是准备,并没有输出
	{
		retry++;
		delay_us(1);
	};
	if(retry>=100)return 1;	    
	return 0;
}

//从DHT11读取一个位
//返回值:1/0
u8 DHT11_Read_Bit(void) 			 
{
 	u8 retry=0;
	while(DHT11_DQ_IN&&retry<100)//等待变为低电平
	{
		retry++;
		delay_us(1);
	}
	retry=0;
	while(!DHT11_DQ_IN&&retry<100)//等待变高电平
	{
		retry++;
		delay_us(1);
	}
	delay_us(40);//等待40us
	if(DHT11_DQ_IN)return 1;
	else return 0;		   
}

//从DHT11读取一个字节
//返回值:读到的数据
u8 DHT11_Read_Byte(void)    
{        
  u8 i,dat;
  dat=0;
	for (i=0;i<8;i++) 
	{
   		dat<<=1; 
	    dat|=DHT11_Read_Bit();
  }						    
  return dat;
}

//从DHT11读取一次数据
//temp:温度值(范围:0~50°)
//humi:湿度值(范围:20%~90%)
//返回值:0,正常;1,读取失败
u8 DHT11_Read_Data()    
{        
 	u8 buf[5];
	u8 i;
	DHT11_Rst();         //main函数对端口初始化时已调用一次,这里调用是为了保证数据最新
	if(DHT11_Check()==0) //main函数对端口初始化时已调用一次,这里调用是为了保证数据最新
	{
		for(i=0;i<5;i++)//读取40位数据
		{
			buf[i]=DHT11_Read_Byte();
		}
		if((buf[0]+buf[1]+buf[2]+buf[3])==buf[4])
		{
		 OLED_ShowNum(74,2,buf[2],2);//显示温度数据整数
	   OLED_ShowNum(96,2,buf[3],1);//显示温度数据小数
	   OLED_ShowNum(74,5,buf[0],2);//显示湿度数据整数
	   OLED_ShowNum(96,5,buf[1],1);//显示湿度数据小数
		}
	}else return 1;
	return 0;	    
}

//初始化DHT11的IO口  同时检测DHT11的存在
//返回1:不存在
//返回0:存在    	 
u8 DHT11_Init(void)
{	 
 	GPIO_InitTypeDef  GPIO_InitStructure;
 	
 	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);	 //使能PB端口时钟
	
 	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;				 //PB0端口配置
 	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; 		 //推挽输出
 	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
 	GPIO_Init(GPIOB, &GPIO_InitStructure);				 //初始化IO口
 	GPIO_SetBits(GPIOB,GPIO_Pin_0);						 //PB0 输出高
			    
	DHT11_Rst();  //起始信号
	return DHT11_Check();//等待DHT11的回应
} 

OLED.h头文件

#ifndef _OLED_H 
#define _OLED_H
#include "stm32f10x.h"

#define OLED_SCL_Set()    GPIO_SetBits(GPIOA,GPIO_Pin_6);
#define OLED_SCL_CLr()	  GPIO_ResetBits(GPIOA,GPIO_Pin_6);
#define OLED_SDA_Set()    GPIO_SetBits(GPIOA,GPIO_Pin_7);
#define OLED_SDA_CLr()    GPIO_ResetBits(GPIOA,GPIO_Pin_7);
#define OLED_SDA_RCV()		GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_7)

#define OLED_CMD  1
#define OLED_DATA 0

void IIC_config(void);							
void IIC_Start(void);
void IIC_Stop(void);
char OLED_Wait_ACK(void);
void IIC_Send(uint8_t Write_Byte);

void OLED_WR_CMD(uint8_t cmd);
void OLED_WR_DATA(uint8_t data);
void OLED_WR_Byte(uint8_t data,uint8_t cmd);
void OLED_ON(void);
void OLED_OFF(void);
void OLED_Init(void);
void OLED_Clear(void);
void OLED_ShowChar(uint8_t x,uint8_t y, uint8_t ch,uint8_t temp);
void OLED_ShowStr(uint8_t x,uint8_t y,char *ch,uint8_t temp);
int OLED_Getpos(unsigned char length);
void OLED_ShowNum(unsigned char x,unsigned char y,unsigned int Num,unsigned char length);
void OLED_WriteCF(uint8_t x,uint8_t y,uint8_t m,uint8_t server);
void OLED_WriteCN(uint8_t x,uint8_t y,uint8_t m,uint8_t count);
void OLED_ShowBMP(uint8_t x0,uint8_t y0, uint8_t x1,uint8_t y1,uint8_t* BMP);
#endif

fontstr.h头文件
OLED.c文件

#include "stm32f10x.h"
#include "oled.h"
#include "delay.h"
#include "fontstr.h"

//GPIO模拟IIC初始化
void IIC_config()
{
   GPIO_InitTypeDef GPIO_Initstrue;
	
	 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	  
	 GPIO_Initstrue.GPIO_Mode = GPIO_Mode_Out_OD; //开漏输出
	 GPIO_Initstrue.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7 ;
	 GPIO_Initstrue.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init( GPIOA, &GPIO_Initstrue);           //PA6:SCL PA7:SDA
 	 OLED_SCL_Set();
	 OLED_SDA_Set();
}

//模拟IIC起始信号:当SCL为高电平期间,SDA有高到低的跳变
void IIC_Start()
{
   OLED_SCL_Set();   //
	 OLED_SDA_Set();   //空闲状态下拉高时钟总线和数据总线   
	 delay_us(1);
	
	 OLED_SDA_CLr();   //数据总线低电平
	 delay_us(1);
	 
	 OLED_SCL_CLr();   //时钟总线低电平
	 delay_us(1);
}

//模拟IIC停止信号:当SCL为高电平期间,SDA由低到高的跳变
void IIC_Stop()
{
	 OLED_SDA_CLr();   //数据总线低电平
	 delay_us(1);
	 
	 OLED_SCL_Set();   //时钟总线高电平
	 delay_us(1);
	
	 OLED_SDA_Set();   //数据总线高电平
	 delay_us(1);
}

//模拟IIC从机反馈主机应答信号:接收器在“第九个时钟脉冲之前的低电平期间”将SDA线拉低,并且确保在该时钟的高电平期间为稳定的低电平
char OLED_Wait_ACK()
{
	 OLED_SCL_CLr();   //时钟总线低电平
	 delay_us(1);
	
	 OLED_SDA_CLr();   //数据总线低电平
	 delay_us(1);
	
	 OLED_SCL_Set();   //时钟总线高电平
	 delay_us(1);
	  
	 if(OLED_SDA_RCV())//应答信号为低电平时,为有效应答
	 {
	    return 1;      
	 }
	 else              //应答信号为高电平时,为非应答
	 {
		 	OLED_SCL_CLr();//拉低电平接收下一个数据
		  return 0;     
	 }

}

//模拟IIC发送数据:数据在SCL的上升沿到来之前就需准备好。并在下降沿到来之前必须稳定  
//Write_Byte:要发送的数据
void IIC_Send(uint8_t Write_Byte)
{
	 unsigned char i;
	 for(i=0;i<8;i++)            //每个时钟周期发送1个bit大小的数据,八个时钟周期代表一个完整的数据
	 {
			 OLED_SCL_CLr();         //时钟总线低电平
			 delay_us(1);
			
			 if(Write_Byte & 0x80)   //每次从数据的首位开始接收
			 {
						 OLED_SDA_Set();   //数据总线高电平,代表发送 位1
			 }else{
						 OLED_SDA_CLr();   //数据总线低电平,代表发送 位0
			 }		
			 delay_us(1);
			 OLED_SCL_Set();         //时钟总线高电平
			 delay_us(1);	 
			 Write_Byte <<= 1;       
			 delay_us(1);
  }
	while(OLED_Wait_ACK());      //等待从机应答
}

//对OLED写入一个命令
void OLED_WR_CMD(uint8_t cmd)
{
   IIC_Start();    //起始信号
   IIC_Send(0x78); //发送从设备地址,厂家规定0x78即OLED的地址
	 IIC_Send(0x00); //发送从设备内部的寄存器或存储器地址,之后是要对该地址的内容进行读或写
	                 //发送命令的首地址为0x00
	 IIC_Send(cmd);  //发送命令
	 IIC_Stop();     //停止信号
}

//对OLED写入一个数据
void OLED_WR_DATA(uint8_t data)
{
   IIC_Start();    //起始信号
   IIC_Send(0x78); //发送从设备地址,厂家规定0x78即OLED的地址
	 IIC_Send(0x40); //发送从设备内部的寄存器或存储器地址,之后是要对该地址的内容进行读或写
	                 //发送数据的首地址为0x40
	 IIC_Send(data); //发送数据
	 IIC_Stop();     //停止信号
}

//对OLED写入一个字节
//mount
//1:写命令
//0:写数据
void OLED_WR_Byte(uint8_t data,uint8_t mount)
{
   if(mount)
	 {
	    OLED_WR_CMD(data);
	 }else{
	    OLED_WR_DATA(data);
	 }
}

//设置起始地址
void OLED_SetPos(uint8_t x,uint8_t y)
{
   OLED_WR_Byte(0xb0+y,OLED_CMD);           //设置页地址
	 OLED_WR_Byte(0x0f&x,OLED_CMD);           //设置列地址的低四位
	 OLED_WR_Byte((0xf0&x)>>4|0x10,OLED_CMD); //设置列地址的高四位
}

//打开OLED
void OLED_ON()
{
    OLED_WR_Byte(0x8D,OLED_CMD);  //设置电荷泵
	  OLED_WR_Byte(0x14,OLED_CMD);  //开启电荷泵
	  OLED_WR_Byte(0xAF,OLED_CMD);  //OLED唤醒
}

//关闭OLED
void OLED_OFF()
{
    OLED_WR_Byte(0x8D,OLED_CMD);  //设置电荷泵
	  OLED_WR_Byte(0x10,OLED_CMD);  //关闭电荷泵
	  OLED_WR_Byte(0xAE,OLED_CMD);  //关闭OLED
}

//OLED屏幕初始化,厂家自给,不比深究
void OLED_Init()
{
  delay_init();      //延时初始化
  delay_ms(200);     //必须延时
	OLED_WR_CMD(0xAE); //display off
	OLED_WR_CMD(0x20);	//Set Memory Addressing Mode	
	OLED_WR_CMD(0x10);	//00,Horizontal Addressing Mode;01,Vertical Addressing Mode;10,Page Addressing Mode (RESET);11,Invalid
	OLED_WR_CMD(0xb0);	//Set Page Start Address for Page Addressing Mode,0-7
	OLED_WR_CMD(0xc8);	//Set COM Output Scan Direction
	OLED_WR_CMD(0x00); //---set low column address
	OLED_WR_CMD(0x10); //---set high column address
	OLED_WR_CMD(0x40); //--set start line address
	OLED_WR_CMD(0x81); //--set contrast control register
	OLED_WR_CMD(0xff); //áá?èμ÷?ú 0x00~0xff
	OLED_WR_CMD(0xa1); //--set segment re-map 0 to 127
	OLED_WR_CMD(0xa6); //--set normal display
	OLED_WR_CMD(0xa8); //--set multiplex ratio(1 to 64)
	OLED_WR_CMD(0x3F); //
	OLED_WR_CMD(0xa4); //0xa4,Output follows RAM content;0xa5,Output ignores RAM content
	OLED_WR_CMD(0xd3); //-set display offset
	OLED_WR_CMD(0x00); //-not offset
	OLED_WR_CMD(0xd5); //--set display clock divide ratio/oscillator frequency
	OLED_WR_CMD(0xf0); //--set divide ratio
	OLED_WR_CMD(0xd9); //--set pre-charge period
	OLED_WR_CMD(0x22); //
	OLED_WR_CMD(0xda); //--set com pins hardware configuration
	OLED_WR_CMD(0x12);
	OLED_WR_CMD(0xdb); //--set vcomh
	OLED_WR_CMD(0x20); //0x20,0.77xVcc
	OLED_WR_CMD(0x8d); //--set DC-DC enable
	OLED_WR_CMD(0x14); //
	OLED_WR_CMD(0xaf); //--turn on oled panel
}

//清屏
void OLED_Clear()
{
   uint8_t m,n;
	 for(m=0;m<8;m++)
	{
		 OLED_SetPos(0,m);
	   for(n=0;n<128;n++)
		{
		   OLED_WR_Byte(0x00,OLED_DATA);
		}
	}
}

//向OLED写入一个字符
//x:0 ~ 127
//y:0 ~ 63
//ch:写入的字符
//temp:   6:6x8字体  8:8x16字体
void OLED_ShowChar(uint8_t x,uint8_t y, uint8_t ch,uint8_t temp)
{
   unsigned char i = 0,c = 0;
	 if(ch != '\0')
	 {
		   c = ch - 32;
		   OLED_SetPos(x,y);
		   if(temp == 6)
			 {
   		   for(i=0;i<temp;i++)
		        OLED_WR_Byte(F6x8[c][i],OLED_DATA);
		   }else if(temp == 8){
   		   for(i=0;i<temp;i++)
		        OLED_WR_Byte(F8X16[c*16+i],OLED_DATA);
				 OLED_SetPos(x,++y);
				 for(i=0;i<temp;i++)
		        OLED_WR_Byte(F8X16[c*16+i+8],OLED_DATA);
		   }
	 }
}

//显示字符串函数
//x:0 ~ 127
//y:0 ~ 63
//ch:要显示的字符
//temp:   6:6x8字体  8:8x16字体
void OLED_ShowStr(uint8_t x,uint8_t y,char *ch,uint8_t temp)
{ 
   unsigned char j = 0;
	 while(ch[j] != '\0')
	 {
		  if(temp == 6)
   	  {
				 if(x>=122)
				 {
				   x = 0;
					 y++;
				 }
		   }else if(temp == 8){
				 if(x>=120)
				 {
				   x = 0;
					 y+=2;
				 } 
		   }
			 OLED_ShowChar(x,y,ch[j],temp);
			 x += temp;
			 j++;
   }
}

//次方函数
int OLED_Getpos(unsigned char length)
{
  unsigned int pos = 1;
	while(length--)
	{
	   pos *= 10;
	}
	return pos;
}

//显示数字函数
//x:0 ~ 127
//y:0 ~ 63
//Num:要显示的数字
//length:数字的位数 0,1,2,3...
void OLED_ShowNum(unsigned char x,unsigned char y,unsigned int Num,unsigned char length)
{
	unsigned char i;
  for(i=length;i>0;i--)
	{
	  OLED_ShowChar(x,y,'0'+Num/OLED_Getpos(--length)%10,8);	x+=8;
	}
}

//显示单个文字函数
//x,y:坐标
//m:第几个个数组
//server:数组第几个文字
void OLED_WriteCF(uint8_t x,uint8_t y,uint8_t m,uint8_t server)
{
       uint8_t i;
	     if(x>112)
			 {
				   x = 0;
					 y+=2;
			 }
		   OLED_SetPos(x,y);
			 for(i=0;i<16;i++)
				 OLED_WR_Byte(F16x16[m][i+(server-1)*32],OLED_DATA);
			 OLED_SetPos(x,++y);
			 for(i=0;i<16;i++)
				 OLED_WR_Byte(F16x16[m][i+(server-1)*32+16],OLED_DATA);
		   
}

//连续显示文字函数
//x,y:坐标
//m:第几个个数组
//count:连续显示几个文字
void OLED_WriteCN(uint8_t x,uint8_t y,uint8_t m,uint8_t count)
{
   uint8_t i,j = 0;
	 while(count--)
	 {
		   if(x>112)
			 {
				   x = 0;
					 y+=2;
			 }
		   OLED_SetPos(x,y);
			 for(i=0;i<16;i++)
				 OLED_WR_Byte(F16x16[m][i+j*32],OLED_DATA);
			 OLED_SetPos(x,++y);
			 for(i=0;i<16;i++)
				 OLED_WR_Byte(F16x16[m][i+16+j*32],OLED_DATA);
		   x += 16;
   		 OLED_SetPos(x,--y);
		   j++;
	 }
}

//显示BMP图片函数
//(x0,y0):图片显示的开始地址
//(x1,y1):图片显示的结束地址
//BMP:要显示的图片的首地址,具体看fontstr.h文件
void OLED_ShowBMP(uint8_t x0,uint8_t y0, uint8_t x1,uint8_t y1,uint8_t* BMP)
{
		uint8_t x,y;
		for(y=y0;y<y1;y++)
		{
			 OLED_SetPos(x0,y);
			 for(x=x0;x<x1;x++)
			{
				 OLED_WR_Byte(BMP[x+(x1-x0)*y],OLED_DATA);
			}
		}
}

main.c文件

#include "stm32f10x.h"
#include "oled.h"
#include "delay.h"
#include "dht11.h"

int  main()
{
	  delay_init();             //延时初始化
    IIC_config();             //GPIO模拟IIC初始化
    OLED_Init();              //OLED初始化
    OLED_ON();                //打开OLED
    delay_ms(2000);           //延时2s
    OLED_Clear();             //清屏
    delay_ms(2000);           //延时2s

	  OLED_WriteCN(0,2,0,5);    //显示->当前温度:
    OLED_WriteCF(90,2,2,3);   //显示->小数点
    OLED_WriteCF(112,2,2,1);  //显示->℃单位

    OLED_WriteCN(0,5,1,5);    //显示->当前湿度:
    OLED_WriteCF(90,5,2,3);   //显示->小数点
    OLED_WriteCF(112,5,2,2);  //显示%单位
	  while( DHT11_Init() )return 1; //初始化DHT11模块端口,while等待从机DHT11回应
    while(1)
    {
			DHT11_Read_Data();      //读取数据并显示
	    delay_ms(500);
    }
}
  嵌入式 最新文章
基于高精度单片机开发红外测温仪方案
89C51单片机与DAC0832
基于51单片机宠物自动投料喂食器控制系统仿
《痞子衡嵌入式半月刊》 第 68 期
多思计组实验实验七 简单模型机实验
CSC7720
启明智显分享| ESP32学习笔记参考--PWM(脉冲
STM32初探
STM32 总结
【STM32】CubeMX例程四---定时器中断(附工
上一篇文章      下一篇文章      查看所有文章
加:2021-07-27 16:23:50  更:2021-07-27 16:26:22 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/25 18:16:44-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码