라즈베리파이 온습도 제어 시스템을 만들면서
sht20 온습도 센서를 사용했고, 이 과정에서 sht20센서의 디바이스 드라이버를 직접 제작했다.
디바이스 트리를 작성하고, 데이터 시트를 보고 명령어를 짜고 꽤나 공부가 되었던 토이 프로젝트 인데,
정작 리눅스 내에 sht21 드라이버가 있는 줄은 몰랐다.
Yocto-rasp-BSP/meta-mylayer/recipes-kernel/sensor-drivers/files/sht20_driver.c at master · Jminu/Yocto-rasp-BSP
build custom linux for RaspberryPi using Yocto. Contribute to Jminu/Yocto-rasp-BSP development by creating an account on GitHub.
github.com
이게 내가 작성한 sht20 드라이버 코드이고, 글에서는 편하게 JMW드라이버라고 부르겠다. (내 이름)
https://github.com/torvalds/linux/blob/master/drivers/hwmon/sht21.c
linux/drivers/hwmon/sht21.c at master · torvalds/linux
Linux kernel source tree. Contribute to torvalds/linux development by creating an account on GitHub.
github.com
이게 리눅스 메인라인에 들어있는 sht21 드라이버 코드인데, sht20하고 아마 호환이 될 것이다.
probe함수 코드비교
일단 probe 함수부터 비교해보기 전에
probe 함수가 뭔지 간단하게 알아보자.
드라이버 프로브(Driver Probe)는
리눅스 커널에서 특정 하드웨어 장치(Platform Device)를 찾아서 드라이버와 바인딩(연결)하고 초기화하는 핵심 함수
로, 장치가 감지되면 커널이 드라이버 구조체 내의
probe
함수를 호출해 장치 설정, 메모리 할당, 자원 확보 등을 수행하며 장치를 사용 가능하게 만드는 과정입니다.
이게 probe 함수의 정의이다.
긍까 드라이버를 처음 만들고, insmod를 하게되는데 그때 실행되는 함수.
jmw_sht20의 probe
139 static int sht20_probe(struct i2c_client *client, const struct i2c_device_id *id) {
140 struct sht20_device *sht20;
141 int ret;
142
143 sht20 = devm_kzalloc(&client->dev, sizeof(struct sht20_device), GFP_KERNEL); // sht20을 위한 kernel공간 할당
144 if (sht20 == NULL) {
145 printk(KERN_ERR "devm_kzalloc fail\n");
146 return -1;
147 }
148
149 sht20->client = client; // 실제 칩을 연결(client)
150
151 /*
152 * @client: i2c_client구조체안에 dev가 존재, 그 dev안에 driver_data
153 * @sht20: driver_data안에 넣을 데이터
154 */
155 i2c_set_clientdata(client, sht20); // 종료되어도 sht20의 상태를 알 수 있음
156
157 ret = sht20_soft_reset(client); // soft reset 명령 write
158 if (ret < 0)
159 return ret;
160
161 // create char dev, device, class
162 ret = alloc_chrdev_region(&(sht20->dev_num), 0, 1, DEVICE_NAME);
163 if (ret != 0) {
164 printk(KERN_ERR "alloc chrdev region fail\n");
165 return -1;
166 }
167
168 cdev_init(&(sht20->sht20_cdev), &fops);
169 ret = cdev_add(&(sht20->sht20_cdev), sht20->dev_num, DEVICE_COUNT);
170 if (ret < 0) {
171 printk(KERN_ERR "cdev add fail\n");
172 return -1;
173 }
174
175 sht20->class = class_create(THIS_MODULE, CLASS_NAME);
176 device_create(sht20->class, NULL, sht20->dev_num, NULL, DEVICE_NAME);
177
178 return 0;
179 }
- devm_kzalloc(&client->dev, sizeof(struct sht20_device), GFP_KERNEL): 디바이스 위한 커널 공간 할당.
- i2c_set_clientdata(client, sht20): client->data에 sht20포인터를 넣는다.
- https://elixir.bootlin.com/linux/v6.13.2/source/include/linux/device.h#L943 를 확인해보자. 정확히 어떤 의미인지 모르겠음.
- ret = sht20_soft_reset(client): soft reset 명령을 내림.
- #define SOFT_RESET 0xFE로 정의했음. This command (see Table 6) is used for rebooting the
sensor system without switching the power off and on again. 데이터 시트에 soft reset의 정의.
- #define SOFT_RESET 0xFE로 정의했음. This command (see Table 6) is used for rebooting the
- alloc_chrdev_region(&(sht20->dev_num), 0, 1, DEVICE_NAME): 디바이스 넘버 할당받음 by Kernel
- cdev_init(&(sht20->sht20_cdev), &fops): initialize cdev structure, 만들었던 fops등록
- ret = cdev_add(&(sht20->sht20_cdev), &fops): 문자 디바이스를 시스템에 등록 -> 특정 디바이스 번호로 들어오는 신호는 방금 등록했던 fops를 통해서 처리한다
- sht20->class = class_create(THIS_MODULE, CLASS_NAME): create a struct class structure
- device_create(sht20->class, NULL, sht20->dev_num, NULL, DEVICE_NAME): sysfs에 등록, /dev 에 장치파일을 생성한다.
sht20_soft_reset
45 static int sht20_soft_reset(struct i2c_client *client) {
46 int ret = i2c_smbus_write_byte(client, SOFT_RESET); // write SOFT_RESET command to SHT20
47 if (ret < 0) {
48 printk(KERN_ERR "i2c smbus write fail\n");
49 return -1;
50 }
51 msleep(100);
52
53 return ret;
54 }
- i2c_smbus_write_byte(client, SOFT_RESET): https://elixir.bootlin.com/linux/v6.13.2/source/drivers/i2c/i2c-core-smbus.c#L114 를 보면 smbus write는 'send byte' 프로토콜임.
linux_sht21의 probe
248 ATTRIBUTE_GROUPS(sht21);
249
250 static int sht21_probe(struct i2c_client *client)
251 {
252 struct device *dev = &client->dev;
253 struct device *hwmon_dev;
254 struct sht21 *sht21;
255
256 if (!i2c_check_functionality(client->adapter,
257 I2C_FUNC_SMBUS_WORD_DATA)) {
258 dev_err(&client->dev,
259 "adapter does not support SMBus word transactions\n");
260 return -ENODEV;
261 }
262
263 sht21 = devm_kzalloc(dev, sizeof(*sht21), GFP_KERNEL);
264 if (!sht21)
265 return -ENOMEM;
266
267 sht21->client = client;
268
269 mutex_init(&sht21->lock);
270
271 hwmon_dev = devm_hwmon_device_register_with_groups(dev, client->name,
272 sht21, sht21_groups);
273 return PTR_ERR_OR_ZERO(hwmon_dev);
274 }
- i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_WORD_DATA): 어댑터 사용가능한지 판단
- devm_kzalloc(dev, sizeof(*sht21), GFP_KERNEL): 디바이스 공간 할당
- devm_kmalloc: 디바이스를 위한 메모리 할당. devm_kzalloc은 할당해준다음 0으로 초기화함
- https://elixir.bootlin.com/linux/v6.13.2/source/drivers/base/devres.c#L822
- sht21->client = client: 전달받은 i2c_client를 sht21->client에 바인딩
- 그리고 리눅스 메인라인 코드에서는 mutex_init이랑 devm_hwmon_device_register_with_groups를 사용함 (jmw_sht20에서도 뮤텍스를 쓰는걸 고려하는게 좋을 듯)
온도/습도 읽어오는 방식
jmw_sht20
에서는 read_data -> sht20_read_data를 통해서 읽어온다.
89 static ssize_t sht20_read(struct file *file, char __user *buf, size_t len, loff_t *pos) {
90 struct sht20_device *sht20 = file->private_data;
91 int temp_raw;
92 int humid_raw;
93 char kbuf[64];
94
95 printk(KERN_INFO "sht20_driver.c: sht20 read\n");
96
97 if (*pos > 0) {
98 printk(KERN_ERR "pos err\n");
99 return -1;
100 }
101
102 int ret;
103
104 ret = sht20_read_data(sht20->client, TEMP_MEASUREMENT, &temp_raw); // 0x40 chip address를 대상으로 온도 측정 명령
105 if (ret < 0) {
106 printk(KERN_ERR "Temp measurement fail\n");
107 return -1;
108 }
109
110 ret = sht20_read_data(sht20->client, HUMID_MEASUREMENT, &humid_raw) ; // 0x40 chip address를 대상으로 습고 측정 명령
111 if (ret < 0) {
112 printk(KERN_ERR "Humid measurement fail\n");
113 return -1;
114 }
115
116 len = snprintf(kbuf, sizeof(kbuf), "%d|%d", temp_raw, humid_raw);
117 // printk(KERN_INFO "%s\n", kbuf);
118
119 copy_to_user(buf, kbuf, len);
120
121 return len;
122 }
- struct sht20_device *sht20 = file->private_data: private_data는 file system or driver specific data라고 한다.
- ret = sht20_read_data(sht20->client, TEMP_MEASUREMENT, &temp_raw): 온도데이터 읽어옴. sht20_read_data는 좀있다 다루겠음
- len = snprintf(kbuf, sizeof(buf), %d|%d", temp_raw, humid_raw): 문자열로 만들어서
- copy_to_user(buf, kbuf, len): 유저 공간으로 문자열을 보냄
sht20_read의 초기부분에 struct sht20_device *sht20 = file->private_data를 해서 devm_kzalloc으로 커널이 할당해주었던 공간의 주소를 반환받는다.
file->private_data는 어디서?
124 static int sht20_open(struct inode *inode, struct file *file) {
125 struct sht20_device *sht20;
126 sht20 = container_of(inode->i_cdev, struct sht20_device, sht20_cdev);
127 file->private_data = sht20; // 센서 데이터에 접근가능 ex)temp
128
129 return 0;
130 }
- sht20 = container_of(inode->i_cdev, struct sht20_device, sht20_cdev): 커널이 할당해주었던 공간의 주소를 반환
- file->private_data: 여기서 그 주소가 들어있음. 따라서 위의 sht20_read에서 이방식으로 구조체의 주소를 가져옴
- container_of와 file->private_data에서의 데이터 전달 중요
실제로 온도/습도를 읽어오는 곳
sht20_read_data
63 static int sht20_read_data(struct i2c_client *client, int command, int *val) {
64 int ret;
65 u8 buf[3]; // 데이터 받을 unsigned char 3byte
66
67 ret = i2c_smbus_write_byte(client, command); // write command to sht20
68 if (ret < 0) {
69 printk(KERN_ERR "i2c_smbus_write_byte Fail\n");
70 return -1;
71 }
72
73 msleep(100);
74
75 ret = i2c_master_recv(client, buf, 3); // SHT20으로부터 word만큼 데이터 읽음(3byte)
76 if (ret < 0) {
77 printk(KERN_ERR "i2c_master_recv Fail\n");
78 return -1;
79 }
80
81 *val = (buf[0] << 8) | (buf[1] & 0xFC); // buf[1]에서 하위 2비트는 stat비트이기 때문에 무시
82
83 return 0;
84 }
- ret = i2c_smbus_write_byte(client, command): client즉 sht20에 command 여기서는 TEMP_MEASUREMENT 명령을 내린다. 명령내리고 msleep(100)으로 기다림.
- 여기서 client는 sht20이 되겠고, i2c주소 + write = 1000 0000 이다. (LSB가 1이면 read, 0이면 write)
- ret = i2c_master_recv(client, buf, 3): sht20으로부터 word만큼 데이터를 읽는다. (3byte) 왜 3바이트를 읽는가?
- 왜냐면 no hold master mode에서 read를 했을 때, 3바이트가 필요함. 근데 여기서 Data (LSB)의 마지막 2비트는 상태비트니까 제외시키자.

linux_sht21
리눅스에 있는 메인라인 코드에서는
sht21_temperature_show에서 sht21_update_measurement를 호출하면서 온도를 읽어온다.
sht21_temperature_show
125 static ssize_t sht21_temperature_show(struct device *dev,
126 struct device_attribute *attr,
127 char *buf)
128 {
129 struct sht21 *sht21 = dev_get_drvdata(dev);
130 int ret;
131
132 ret = sht21_update_measurements(dev);
133 if (ret < 0)
134 return ret;
135 return sprintf(buf, "%d\n", sht21->temperature);
136 }
- struct sht21 *sht21 = dev_get_drvdata(dev): 아마 커널내에 디바이스를 위해 할당된 공간을 가리키는 포인터로 추정
- ret = sht21_update_measurements(dev) 호출
sht21_update_measurements
84 static int sht21_update_measurements(struct device *dev)
85 {
86 int ret = 0;
87 struct sht21 *sht21 = dev_get_drvdata(dev);
88 struct i2c_client *client = sht21->client;
89
90 mutex_lock(&sht21->lock);
91 /*
92 * Data sheet 2.4:
93 * SHT2x should not be active for more than 10% of the time - e.g.
94 * maximum two measurements per second at 12bit accuracy shall be made.
95 */
96 if (time_after(jiffies, sht21->last_update + HZ / 2) || !sht21->valid) {
97 ret = i2c_smbus_read_word_swapped(client,
98 SHT21_TRIG_T_MEASUREMENT_HM);
99 if (ret < 0)
100 goto out;
101 sht21->temperature = sht21_temp_ticks_to_millicelsius(ret);
102 ret = i2c_smbus_read_word_swapped(client,
103 SHT21_TRIG_RH_MEASUREMENT_HM);
104 if (ret < 0)
105 goto out;
106 sht21->humidity = sht21_rh_ticks_to_per_cent_mille(ret);
107 sht21->last_update = jiffies;
108 sht21->valid = true;
109 }
110 out:
111 mutex_unlock(&sht21->lock);
112
113 return ret >= 0 ? 0 : ret;
114 }
- mutex_lock(&sht21->lock): 데이터 읽기전에 lock 걸어놓음
- sht21->temperature = sht21_temp_ticks_to_millicelsius(ret): 온도 변환해주는 공식으로 추측
- ret = i2c_smbus_read_word_swapped(client, SHT21_TRIG_T_MEASUREMENT_HM)
- 내부적으로 i2c_smbus_read_word_data 호출: 즉, word만큼 데이터 읽어옴.
- sht21->humidity = sht21_rh_ticks_to_millicelsius(ret): 습도 변환해주는 공식으로 추측
- mutex_unlock(&sht21->lock): 다 읽었으니 뮤텍스 락 해제
전체적으로 다른 점
리눅스 메인라인에 있는 sht21드라이버는
- 뮤텍스 사용
- hwmon 프레임 워크사용
- devm계열의 함수를 사용하여 좀 더 자동화되고 깔끔한 코드
- jiffies사용 -> 왜 사용했을까?