안드로이드 TP 제어 분석
1. TP의 하드웨어 인터페이스
발붙이다
명칭 및 역할
VDD
TP 전원 공급
RESET
리드 백
EINT
발길을 끊다
SCL、SDA
I2C 커넥터
TP의 작동 방식은 간단합니다.
2. 코드 경로
묘사
경로
서류
시스템 설정
device\top\top6737t_36_a_m0
kernel-3.18\arch\arm\configs
ProjectConfig.mk
user버전과userdebug버전의 대응: top6737t_36_a_m0_defconfig
debug버전의 대응: top6737t_36_a_m0_debug_defconfig
인터페이스 설정kernel-3.18\arch\arm\boot\dts
top6737t_36_a_m0.dtscust_i2c.dtsi
Kernel 코드
kernel-3.18\drivers\input\touchscreen\mediatek
mtk_tpd.c 등
3. TP 코드 분석
1. 하드웨어 파라미터 설정
dts 파일에서 하드웨어 인터페이스를 대조하여 GPIO 설정을 수정하고 TP의 가상 키 좌표, 해상도 등 파라미터 정보를 수정한다.
// top6737t_36_a_m0.dts
......
&accdet {
pinctrl-names = "default","state_eint_as_int";
pinctrl-0 = ;
pinctrl-1 = ;
status = "okay";
};
&pio {
ACCDET_pins_default: eint6default {
};
ACCDET_pins_eint_int: eint@6 {
pins_cmd_dat {
pins = ;
slew-rate = <0>;
bias-disable;
};
};
};
&touch {
tpd-resolution = <240 240>;
use-tpd-button = <0>;
tpd-key-num = <0>;
tpd-key-local= <139 172 158 0>;
tpd-key-dim-local = <90 883 100 40 230 883 100 40 370 883 100 40 0 0 0 0>;
tpd-max-touch-num = <1>;
tpd-filter-enable = <1>;
tpd-filter-pixel-density = <124>;
tpd-filter-custom-prameters = <0 0 0 0 0 0 0 0 0 0 0 0>;
tpd-filter-custom-speed = <0 0 0>;
pinctrl-names = "default", "state_eint_as_int", "state_eint_output0", "state_eint_output1",
"state_rst_output0", "state_rst_output1";
pinctrl-0 = ;
pinctrl-1 = ;
pinctrl-2 = ;
pinctrl-3 = ;
pinctrl-4 = ;
pinctrl-5 = ;
status = "okay";
};
......
해당 I2C 설정도 있습니다.
/* cust_i2c.dtsi */
&i2c1 {
cap_touch@5d {
compatible = "mediatek,cap_touch";
reg = <0x5d>;
};
};
2. TP 장치 구동
파일
mtk_tpd.c
은 TP설비 구동의 입구로 본 파일을 분석하여 코드 구조를 간단하게 요약할 수 있다.우선,platform을 통해driver_register는 장치 드라이브를 등록합니다.
//mtk_tpd.c
static int __init tpd_device_init(void)
{
TPD_DEBUG("MediaTek touch panel driver init
");
if (platform_driver_register(&tpd_driver) != 0) {
TPD_DMESG("unable to register touch panel driver.
");
return -1;
}
return 0;
}
//
static struct platform_driver tpd_driver = {
.remove = tpd_remove,
.shutdown = NULL,
.probe = tpd_probe,
.driver = {
.name = TPD_DEVICE,
.pm = &tpd_pm_ops,
.owner = THIS_MODULE,
.of_match_table = touch_of_match,
},
};
이 장치가 구동하는 프로브 함수를 살펴보겠습니다.
//mtk_tpd.c
static int tpd_probe(struct platform_device *pdev)
{
......
// ,
if (misc_register(&tpd_misc_device))
{
pr_err("mtk_tpd: tpd_misc_device register failed
");
}
// dts gpio
tpd_get_gpio_info(pdev);
tpd = kmalloc(sizeof(struct tpd_device), GFP_KERNEL);
if (tpd == NULL)
return -ENOMEM;
memset(tpd, 0, sizeof(struct tpd_device));
// ,
tpd->dev = input_allocate_device();
if (tpd->dev == NULL) {
kfree(tpd);
return -ENOMEM;
}
#ifdef CONFIG_MTK_LCM_PHYSICAL_ROTATION
// ,TP LCD
if (0 == strncmp(CONFIG_MTK_LCM_PHYSICAL_ROTATION, "90", 2) || 0 == strncmp(CONFIG_MTK_LCM_PHYSICAL_ROTATION, "270", 3))
{
#ifdef CONFIG_MTK_FB /*Fix build errors,as some projects cannot support these apis while bring up*/
TPD_RES_Y = DISP_GetScreenWidth();
TPD_RES_X = DISP_GetScreenHeight();
#endif
}
else
#endif
{
#ifdef CONFIG_CUSTOM_LCM_X
#ifndef CONFIG_MTK_FPGA
#ifdef CONFIG_MTK_FB /*Fix build errors,as some projects cannot support these apis while bring up*/
TPD_RES_X = DISP_GetScreenWidth();
TPD_RES_Y = DISP_GetScreenHeight();
#endif
#endif
#else
#ifdef CONFIG_LCM_WIDTH
ret = kstrtoul(CONFIG_LCM_WIDTH, 0, &tpd_res_x);
if (ret < 0) {
pr_err("Touch down get lcm_x failed");
return ret;
}
TPD_RES_X = tpd_res_x;
ret = kstrtoul(CONFIG_LCM_HEIGHT, 0, &tpd_res_x);*/
if (ret < 0) {
pr_err("Touch down get lcm_y failed");
return ret;
}
TPD_RES_Y = tpd_res_y;
#endif
#endif
}
if (2560 == TPD_RES_X)
TPD_RES_X = 2048;
if (1600 == TPD_RES_Y)
TPD_RES_Y = 1536;
pr_info("mtk_tpd: TPD_RES_X = %lu, TPD_RES_Y = %lu
", TPD_RES_X, TPD_RES_Y);
// , ;
tpd_mode = TPD_MODE_NORMAL;
tpd_mode_axis = 0;
tpd_mode_min = TPD_RES_Y / 2;
tpd_mode_max = TPD_RES_Y;
tpd_mode_keypad_tolerance = TPD_RES_X * TPD_RES_X / 1600;
//
tpd->dev->name = TPD_DEVICE;
set_bit(EV_ABS, tpd->dev->evbit);
set_bit(EV_KEY, tpd->dev->evbit);
set_bit(ABS_X, tpd->dev->absbit);
set_bit(ABS_Y, tpd->dev->absbit);
set_bit(ABS_PRESSURE, tpd->dev->absbit);
#if !defined(CONFIG_MTK_S3320) && !defined(CONFIG_MTK_S3320_47)\
&& !defined(CONFIG_MTK_S3320_50) && !defined(CONFIG_MTK_MIT200) \
&& !defined(CONFIG_TOUCHSCREEN_SYNAPTICS_S3528) && !defined(CONFIG_MTK_S7020)
set_bit(BTN_TOUCH, tpd->dev->keybit);
#endif /* CONFIG_MTK_S3320 */
set_bit(INPUT_PROP_DIRECT, tpd->dev->propbit);
// platform_device ;
tpd->tpd_dev = &pdev->dev;
// tpd_driver_list, TP;
for (i = 1; i < TP_DRV_MAX_COUNT; i++)
{
if (tpd_driver_list[i].tpd_device_name != NULL)
{
tpd_driver_list[i].tpd_local_init();
if (tpd_load_status == 1)
{
TPD_DMESG("[mtk-tpd]tpd_probe, tpd_driver_name=%s
",tpd_driver_list[i].tpd_device_name);
g_tpd_drv = &tpd_driver_list[i];
break;
}
}
}
// tpd_driver_list TP , index=0 TP;
if (g_tpd_drv == NULL)
{
if (tpd_driver_list[0].tpd_device_name != NULL)
{
g_tpd_drv = &tpd_driver_list[0];
/* touch_type:0: r-touch, 1: C-touch */
touch_type = 0;
g_tpd_drv->tpd_local_init();
TPD_DMESG("[mtk-tpd]Generic touch panel driver
");
} else {
TPD_DMESG("[mtk-tpd]cap touch and Generic touch both are not loaded!!
");
return 0;
}
}
// TP ;
touch_resume_workqueue = create_singlethread_workqueue("touch_resume");
INIT_WORK(&touch_resume_work, touch_resume_workqueue_callback);
// , TP, TP;
tpd_fb_notifier.notifier_call = tpd_fb_notifier_callback;
if (fb_register_client(&tpd_fb_notifier))
TPD_DMESG("register fb_notifier fail!
");
// TP ;
if (touch_type == 1)
{
set_bit(ABS_MT_TRACKING_ID, tpd->dev->absbit);
set_bit(ABS_MT_TOUCH_MAJOR, tpd->dev->absbit);
set_bit(ABS_MT_TOUCH_MINOR, tpd->dev->absbit);
set_bit(ABS_MT_POSITION_X, tpd->dev->absbit);
set_bit(ABS_MT_POSITION_Y, tpd->dev->absbit);
input_set_abs_params(tpd->dev, ABS_MT_POSITION_X, 0, TPD_RES_X, 0, 0);
input_set_abs_params(tpd->dev, ABS_MT_POSITION_Y, 0, TPD_RES_Y, 0, 0);
#if defined(CONFIG_MTK_S3320) || defined(CONFIG_MTK_S3320_47) \
|| defined(CONFIG_MTK_S3320_50) || defined(CONFIG_MTK_MIT200) \
|| defined(CONFIG_TOUCHSCREEN_SYNAPTICS_S3528) || defined(CONFIG_MTK_S7020)
input_set_abs_params(tpd->dev, ABS_MT_PRESSURE, 0, 255, 0, 0);
input_set_abs_params(tpd->dev, ABS_MT_WIDTH_MAJOR, 0, 15, 0, 0);
input_set_abs_params(tpd->dev, ABS_MT_WIDTH_MINOR, 0, 15, 0, 0);
input_mt_init_slots(tpd->dev, 10, 0);
#else
input_set_abs_params(tpd->dev, ABS_MT_TOUCH_MAJOR, 0, 100, 0, 0);
input_set_abs_params(tpd->dev, ABS_MT_TOUCH_MINOR, 0, 100, 0, 0);
#endif /* CONFIG_MTK_S3320 */
TPD_DMESG("Cap touch panel driver
");
}
input_set_abs_params(tpd->dev, ABS_X, 0, TPD_RES_X, 0, 0);
input_set_abs_params(tpd->dev, ABS_Y, 0, TPD_RES_Y, 0, 0);
input_abs_set_res(tpd->dev, ABS_X, TPD_RES_X);
input_abs_set_res(tpd->dev, ABS_Y, TPD_RES_Y);
input_set_abs_params(tpd->dev, ABS_PRESSURE, 0, 255, 0, 0);
input_set_abs_params(tpd->dev, ABS_MT_TRACKING_ID, 0, 10, 0, 0);
//
if (input_register_device(tpd->dev))
TPD_DMESG("input_register_device failed.(tpd)
");
else
tpd_register_flag = 1;
// TP , ;
if (g_tpd_drv->tpd_have_button)
tpd_button_init();
//
if (g_tpd_drv->attrs.num)
tpd_create_attributes(&pdev->dev, &g_tpd_drv->attrs);
return 0;
}
probe 함수를 실행하면 TP의 장치 노드가 생성됩니다.
probe에서 현재 사용 중인 TP를 찾는 것은 지원 목록 - tpddriver_list에서 구현한 이 목록의 요소는 TP 작업의 구체적인 함수로 구성된 구조체로 다음과 같이 정의됩니다.
//tpd.h
struct tpd_driver_t {
char *tpd_device_name; //TP
int (*tpd_local_init)(void); //TP
void (*suspend)(struct device *h);//TP
void (*resume)(struct device *h);//TP
int tpd_have_button;//
struct tpd_attrs attrs;//
};
서로 다른 TP는 이 구조체의 내용을 실현하고 TP가 해야 할 일을 완성해야 한다.
tpd_driver_list 이 목록 시 호출 함수 tpddriver_add에서 요소를 추가하는 방법:
//mtk_tpd.c
int tpd_driver_add(struct tpd_driver_t *tpd_drv)
{
......
tpd_drv->tpd_have_button = tpd_dts_data.use_tpd_button;
// , index=0 ;
if (strcmp(tpd_drv->tpd_device_name, "generic") == 0) {
tpd_driver_list[0].tpd_device_name = tpd_drv->tpd_device_name;
tpd_driver_list[0].tpd_local_init = tpd_drv->tpd_local_init;
tpd_driver_list[0].suspend = tpd_drv->suspend;
tpd_driver_list[0].resume = tpd_drv->resume;
tpd_driver_list[0].tpd_have_button = tpd_drv->tpd_have_button;
return 0;
}
// , ;
for (i = 1; i < TP_DRV_MAX_COUNT; i++) {
if (tpd_driver_list[i].tpd_device_name == NULL) {
tpd_driver_list[i].tpd_device_name = tpd_drv->tpd_device_name;
tpd_driver_list[i].tpd_local_init = tpd_drv->tpd_local_init;
tpd_driver_list[i].suspend = tpd_drv->suspend;
tpd_driver_list[i].resume = tpd_drv->resume;
tpd_driver_list[i].tpd_have_button = tpd_drv->tpd_have_button;
tpd_driver_list[i].attrs = tpd_drv->attrs;
break;
}
// TP, ;
if (strcmp(tpd_driver_list[i].tpd_device_name, tpd_drv->tpd_device_name) == 0)
return 1;
}
return 0;
}
또한 probe 함수에는 다음과 같은 TP의 백라이트 변경에 따른 작동 방식을 제어하는 백라이트 알림 콜백 함수가 등록되어 있습니다.
//mtk_tpd.c
static int tpd_fb_notifier_callback(struct notifier_block *self, unsigned long event, void *data)
{
......
switch (blank) {
//
case FB_BLANK_UNBLANK:
TPD_DMESG("LCD ON Notify
");
if (g_tpd_drv && tpd_suspend_flag) {
// , TP
err = queue_work(touch_resume_workqueue, &touch_resume_work);
if (!err) {
TPD_DMESG("start touch_resume_workqueue failed
");
return err;
}
}
break;
//
case FB_BLANK_POWERDOWN:
TPD_DMESG("LCD OFF Notify
");
if (g_tpd_drv)
{
// ,
err = cancel_work_sync(&touch_resume_work);
if (!err)
TPD_DMESG("cancel touch_resume_workqueue err = %d
", err);
// TP
g_tpd_drv->suspend(NULL);
tpd_suspend_flag = 1;
}
break;
default:
break;
}
return 0;
}
TP의 장치 구동 기본 구조는 바로 이것들이다.
3. TP 모듈 구동
TP 장치 구동은 TP 구동의 구조를 갖추고 서로 다른 TP 모듈은 이 구조에 따라 주로 구조체 tpd 를 실현한다.driver_t의 내용, 그리고 함수 tpd 를 통해driver_dd가 드라이버 목록에 추가되었습니다.다음은 실례인 ST16XX로 TP 모듈의 드라이버 코드를 살펴보겠습니다.
먼저module을 통해init 및 moduleexit는 모듈을 초기화하고 종료합니다.
//ST16XX_driver.c
static int __init tpd_driver_init(void)
{
// dts , 、
tpd_get_dts_info();
TPD_DMESG("Sitronix st16xx touch panel driver init
");
// tpd_driver_t
if(tpd_driver_add(&tpd_device_driver) < 0)
TPD_DMESG("add generic driver failed
");
return 0;
}
static void __exit tpd_driver_exit(void)
{
TPD_DMESG("MediaTek ST16XX touch panel driver exit
");
// tpd_driver_t
tpd_driver_remove(&tpd_device_driver);
}
module_init(tpd_driver_init);
module_exit(tpd_driver_exit);
구조체 tpddriver_t의 구체적인 실현은 다음과 같다.
//ST16XX_driver.c
static struct tpd_driver_t tpd_device_driver = {
.tpd_device_name =CTP_NAME, // TPD_DEVICE,
.tpd_local_init = tpd_local_init,
.suspend = tpd_suspend,
.resume = tpd_resume,
#ifdef TPD_HAVE_BUTTON
.tpd_have_button = 1,
#else
.tpd_have_button = 0,
#endif
};
여기서 tpdlocal_init 소스는 다음과 같습니다.
//ST16XX_driver.c
static int tpd_local_init(void)
{
// I2C , TP
if(i2c_add_driver(&tpd_i2c_driver)!=0) {
TPD_DMESG("unable to add i2c driver.
");
return -1;
}
// I2C , , I2C
if (tpd_load_status == 0) {
TPD_DMESG("add error touch panel driver.");
i2c_del_driver(&tpd_i2c_driver);
return -1;
}
......
//
tpd_type_cap = 1;
return 0;
}
보시다시피 I2C 드라이브를 추가하는 동작은 tpdlocal_init의 포인트, tpdi2c_driver 구현은 다음과 같습니다.
//ST16XX_driver.c
static struct i2c_client *i2c_client = NULL;
static const struct i2c_device_id tpd_i2c_id[] ={
{
CTP_NAME,0},{
}};
static const struct of_device_id tpd_of_match[] = {
{
.compatible = "mediatek,cap_touch"},
{
},
};
static struct i2c_driver tpd_i2c_driver = {
.driver = {
.name = CTP_NAME,
#ifdef CONFIG_OF
.of_match_table = tpd_of_match,
#endif
.owner = THIS_MODULE,
},
.probe = tpd_i2c_probe,
.remove = tpd_i2c_remove,
.detect = tpd_i2c_detect,
.driver.name = CTP_NAME,
.id_table = tpd_i2c_id,
};
probe 함수를 읽어야 합니다.
//ST16XX_driver.c
static int tpd_i2c_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
......
// I2C
client->addr = 0x55;
i2c_client = client;
TPD_DMESG("Sitronix st16xx touch panel i2c probe:%s
", id->name);
// TP
tpd->reg = regulator_get(tpd->tpd_dev, "vtouch");
if (IS_ERR(tpd->reg))
{
TPD_DMESG("regulator_get() failed!
");
}
err = regulator_set_voltage(tpd->reg, 2800000, 2800000);
if (err)
{
TPD_DMESG("regulator_set_voltage() failed!
");
}
err = regulator_enable(tpd->reg);
if (err != 0)
{
TPD_DMESG("Failed to enable reg-vtouch: %d
", err);
}
// RESET , TP
tpd_gpio_output(GTP_RST_PORT, 1);
msleep(10);
tpd_gpio_output(GTP_RST_PORT, 0);
msleep(10);
tpd_gpio_output(GTP_RST_PORT, 1);
msleep(300);
// TP ID
retval = tpd_get_st16xx_id();
if (retval < 0)
{
tpd_load_status = 0;
TPD_DMESG("tpd_get_st16xx_id error, Maybe not ST16XX
");
return -1;
}
#ifdef I2C_SUPPORT_RS_DMA
// DMA , DMA ;
I2CDMABuf_va = (u8 *)dma_alloc_coherent(NULL, 4096, &I2CDMABuf_pa, GFP_KERNEL);
if(!I2CDMABuf_va)
{
TPD_DMESG("st16xx Allocate Touch DMA I2C Buffer failed!
");
return -1;
}
#endif
// EINT
tpd_irq_registration();
msleep(100);
// TP
status = st16xx_get_status();
if(status != 6)
{
st16xx_print_version(); // TP
tpd_halt = 1;
// upgrade panel here
tpd_halt = 0;
}
//TP
#ifdef ST_UPGRADE_FIRMWARE
#ifdef ST_FIREWARE_FILE
kthread_run(st_upgrade_fw, "Sitronix", "sitronix_update");
#else
st_upgrade_fw();
#endif
#endif
// , EINT
thread = kthread_run(touch_event_handler, 0, CTP_NAME);
if (IS_ERR(thread)) {
err = PTR_ERR(thread);
TPD_DMESG(CTP_NAME " st16xx failed to create kernel thread: %d
", err);
}
// ,TP
tpd_load_status = 1;
return 0;
}
그런 다음 EINT 중단 관련 코드를 살펴보겠습니다.
//ST16XX_driver.c
static int tpd_irq_registration(void)
{
......
// ,
node = of_find_matching_node(node, touch_of_match);
if (node)
{
//
of_property_read_u32_array(node, "debounce", ints, ARRAY_SIZE(ints));
gpio_set_debounce(ints[0], ints[1]);
// , 、 、 ;
touch_irq = irq_of_parse_and_map(node, 0);
ret = request_irq(touch_irq, tpd_eint_interrupt_handler, IRQF_TRIGGER_FALLING,
"TOUCH_PANEL-eint", NULL); //IRQF_TRIGGER_FALLING IRQF_TRIGGER_RISING
if (ret > 0) {
ret = -1;
TPD_DEBUG("tpd request_irq IRQ LINE NOT AVAILABLE!.
");
}
}
......
return ret;
}
인터럽트 콜백 함수를 보려면 다음과 같이 하십시오.
//ST16XX_driver.c
static irqreturn_t tpd_eint_interrupt_handler(int irq, void *dev_id)
{
tpd_flag = 1;
wake_up_interruptible(&waiter);//
return IRQ_HANDLED;
}
인터럽트 리셋은 EINT가 인터럽트한 상반부, 상반부는 대기 대기열을 깨우는 것임을 알 수 있다.probe 함수에 스레드가 생성되어 ENIT 중단의 하반부가 스레드에 배치됩니다.
//ST16XX_driver.c
static DECLARE_WAIT_QUEUE_HEAD(waiter); //
static int touch_event_handler( void *unused )
{
struct sched_param param = {
.sched_priority = RTPM_PRIO_TPD };
unsigned char buf[ST16XX_MAX_TOUCHES*4];
int ret = 0,i=0;
int x,y;
u8 touchCount=0;
// : ;
sched_setscheduler(current, SCHED_RR, ¶m);
do
{
set_current_state(TASK_INTERRUPTIBLE);
while (tpd_halt) {
tpd_flag = 0; msleep(20);}
// ;
wait_event_interruptible(waiter, tpd_flag != 0);
tpd_flag = 0;
TPD_DEBUG_SET_TIME;
set_current_state(TASK_RUNNING);
touchCount=0;
// I2C TP
ret = tpd_i2c_read(i2c_client, buf, ST16XX_MAX_TOUCHES*4, 0x12);
// TP
for(i=0;i<ST16XX_MAX_TOUCHES;i++)
{
if(buf[4*i] & 0x80)
{
x = (int)(buf[i*4] & 0x70) << 4 | buf[i * 4 + 1];
y = (int)(buf[i*4] & 0x07) << 8 | buf[i * 4 + 2];
touchCount++;
tpd_down(0,0, x, y, i);
}
}
if(touchCount == 0)
{
tpd_up(0,0, 0,0,0);
}
// TP
input_sync(tpd->dev);
} while (!kthread_should_stop());
return 0;
}
TP는 데이터 에스컬레이션 프로세스를 중단함으로써 이 슬라이드에 나와 있습니다.
마지막으로 TP의 휴면과 깨우기가 무엇을 하는지 살펴보자.
//ST16XX_driver.c
//
static void tpd_suspend(struct device *h)
{
unsigned char buf[2];
int status;
int ret;
TPD_DEBUG("ST16xx call suspend
");
// I2C, TP
buf[0] = 0x02;
buf[1] = 0x02;
ret = tpd_i2c_write(i2c_client, buf, 2);
msleep(100);
// TP , ;
status = st16xx_get_status();
if(status == 5)
TPD_DEBUG("ST16xx go power down mode success
");
else
TPD_DEBUG("ST16xx go power down mode fail
");
// EINT ;
disable_irq(touch_irq);
tpd_halt = 1;
}
//
static void tpd_resume(struct device *h)
{
TPD_DEBUG("ST16XX call resume
");
// RESET TP
tpd_gpio_output(GTP_RST_PORT, 1);
msleep(10);
tpd_gpio_output(GTP_RST_PORT, 0);
msleep(10);
tpd_gpio_output(GTP_RST_PORT, 1);
msleep(300);
// EINT
enable_irq(touch_irq);
tpd_halt = 0;
}
이로써 TP 드라이브의 핵심 코드가 명확해졌다.
이 내용에 흥미가 있습니까?
현재 기사가 여러분의 문제를 해결하지 못하는 경우 AI 엔진은 머신러닝 분석(스마트 모델이 방금 만들어져 부정확한 경우가 있을 수 있음)을 통해 가장 유사한 기사를 추천합니다:
[BUG 분석] persist 속성이 너무 일찍 설정되어 작동하지 않으며 디스크에 기록되지 않습니다.persist 속성을 너무 일찍 설정하면 작동하지 않으며 디스크에 쓸 수 없습니다. /data/property/디렉터리에 저장된 속성을 불러와서 덮어씁니다.system\core\init\property_service...
텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
CC BY-SA 2.5, CC BY-SA 3.0 및 CC BY-SA 4.0에 따라 라이센스가 부여됩니다.