← 제작기

예제대로 구웠으면 카드가 지워졌다

책상에 둘 작은 화면을 만들고 있습니다. 800×480 크기의 보드에 아이폰 알림과 캘린더 일정, 가족 사진, 시계와 자리의 온습도를 번갈아 띄웁니다. 보드는 Waveshare 제품이고, 예제 코드와 문서가 함께 제공됩니다.

사흘 동안 아홉 번 커밋했는데, 그중 되돌린 곳이 다섯 군데입니다. 다섯 모두 제가 예제나 문서를 그대로 믿은 자리였습니다.

SD 예제를 그대로 구웠다면 사진이 지워졌다

카드에서 사진을 읽으려고 제조사의 SD 예제를 열었습니다. 핀 배치를 확인하려던 것이었는데, 그 예제는 시험 과정 안에서 카드를 포맷합니다. 마운트에 실패했을 때만 도는 옵션과 달리, 포맷 호출이 정상 흐름 한가운데에 그냥 들어 있습니다.

카드에는 원본 사진이 들어 있었습니다. 예제를 굽지 않고 핀 번호만 가져왔고, 제 코드에서는 포맷 옵션을 꺼 두고 쓰기도 photos 폴더로만 한정했습니다.

같은 예제에서 문제가 하나 더 나왔습니다. 백라이트를 켜는 함수가 IO 확장 칩의 출력을 한 바이트 통째로 쓰는데, 그 바이트 안에 SD 칩 선택 핀도 들어 있었습니다. 백라이트를 건드리면 카드 연결이 끊깁니다. 한 비트만 바꾸는 함수를 따로 두었습니다.

예제의 계산이 틀려 있었다

아이폰 알림을 받는 부분은 개발 환경이 제공하는 예제를 옮겨 와 고쳤습니다. 긴 메시지가 44바이트에서 잘렸는데, 처음에는 아이폰 쪽 한도라고 생각했습니다.

실제로는 속성 길이의 윗자리 바이트를 계산하는 곳에서 연산 방향이 반대였습니다. >> 8 이어야 할 것이 << 8 이어서, 제가 요청한 상한 300 이 44 로 전달되고 있었습니다. 0x2C 입니다. 실측해 보니 원문 530바이트 가운데 44바이트만 들어왔고, 그 44 가 어디서 왔는지는 그 한 줄을 보고서야 알았습니다.

고친 뒤에는 512바이트까지 들어옵니다. 1000 을 요청해도 512 에서 끊기므로, 이쪽은 정말 아이폰의 한도로 봅니다. 한도가 둘이었고, 제가 먼저 만난 쪽은 남이 쓴 코드였습니다.

문서가 다른 기종의 것이었다

이 보드는 기본형과 이름이 한 글자 다릅니다. 기본형의 문서를 그대로 옮겨 적었다가 두 번 틀렸습니다. "시계 칩이 없다"고 적었는데 있었고, "외부 연결은 커넥터"라고 적었는데 나사 단자대였습니다. 둘 다 회로도를 열어 보고서야 알았습니다.

그 뒤로는 이 기종의 회로도로만 확인합니다. 문서는 틀리지 않았고, 제가 다른 기기의 문서를 읽고 있었습니다.

선을 흔들면 보드가 재부팅했다

1m 길이의 온습도 센서를 옮기다가 보드가 재부팅했습니다. 로그의 역추적을 풀어 보니 인터럽트 감시 시간 초과였고, 멈춘 곳은 예제가 쓰던 옛 I2C 드라이버의 인터럽트 처리부였습니다. 로그에 찍힌 펌웨어 해시가 방금 구운 것과 같은지도 맞춰 봤습니다.

여기서 짐작으로 고치지 않고 선을 일부러 흔들어 봤습니다. 몇 분 만에 같은 곳에서 세 번 더 멈췄습니다. 재 보기 전에는 그럴듯한 쪽이 맞아 보이는 일은 여기서도 마찬가지였습니다. 비교하려고 선 하나를 아예 뽑아 보니 읽기만 열여섯 번 실패하고 멈추지는 않았습니다. 원인은 선이 끊기는 것보다 흔들릴 때 생기는 잡음이었고, 옛 드라이버가 그 뒤의 인터럽트에서 빠져나오지 못하고 있었습니다.

버스를 한 곳에서 새 드라이버로 열고, 터치와 IO 확장 칩과 센서가 함께 쓰게 했습니다. 두 드라이버를 섞으면 부팅 단계에서 멈추기 때문에 옛 호출을 하나도 남기지 않았고, 빌드 결과물에 옛 파일이 없는 것까지 확인했습니다. 고친 뒤 같은 세기로 두 번 흔들어 봤고, 멈춤은 0번, 센서 읽기 실패는 120번 중 0번이었습니다.

굼뜬 원인은 통신이 아니었다

목록을 끌어 넘기면 굼뜨다는 말을 들었습니다. 센서 때문에 버스를 100kHz 로 낮춰 둔 참이라 그쪽을 의심했습니다. 터치를 읽는 시간을 재 보니 400kHz 에서 0.38ms, 100kHz 에서 1.04ms 였습니다. 세 배 가까이 차이가 나니 원인처럼 보였습니다.

터치만 400kHz 로 올리고 다시 물어봤더니 느낌이 똑같다고 했습니다. 0.6ms 는 손으로 느낄 수 있는 크기가 아니었습니다. 굼뜬 원인은 통신보다 화면을 다시 그리는 쪽에 있었고, 범인이 처음 짚은 곳에 없었던 일이 또 일어난 셈입니다.

그리기 시간을 재 보니, 제조사 기본값인 화면 찢어짐 방지 모드에서는 한 번 그리는 데 평균 77ms 가 걸렸고, 내장 메모리의 40줄짜리 띠로 나눠 그리면 63ms 였습니다. 외부 메모리도 750KB 가 더 남았습니다. 띠 방식으로 바꿨습니다.

대신 잃은 것이 있습니다. 띠 단위로 그리면 페이드 효과가 위에서부터 계단처럼 보여서, 화면 전환 효과와 사진 위의 어두운 막을 모두 뺐습니다. 되돌리는 방법은 설정 파일의 주석에 적어 두었습니다.

남의 코드만 탓할 일은 아니었다

다시 연결할 때마다 재부팅하는 문제가 하나 더 있었는데, 이것은 제 잘못이었습니다. 받은 알림을 담는 1.2KB 짜리 버퍼를 3KB 밖에 되지 않는 블루투스 작업 스택 위에 잡아 둔 탓이었습니다. 고정 메모리로 옮기고 잠금을 걸었습니다. 같은 실수를 사흘 뒤에 4KB 로 한 번 더 했고, 그 일과 조용한 실패 다섯 가지는 13바이트가 넘쳐서 설계가 사라졌다에 적었습니다.

아직 모르는 것

블루투스 전송 속도가 날마다 다릅니다. 같은 펌웨어, 같은 자리에서 초당 8.7KB 부터 60KB 까지 나왔습니다. 연결할 때 요청하는 값은 매번 같게 잡히고, 요청하기 전에는 초당 19KB 였으니 흔들리는 원인이 거기는 아닙니다. 원인은 아직 확정하지 못했습니다. 느릴 때는 사진 한 장에 7~12초, 펌웨어 교체에 6~10분이 걸립니다.

온습도 센서를 집에 있던 온습도계와 나란히 두니, 온도는 26.1도로 같고 습도는 6% 다릅니다. 어느 쪽이 맞는지는 아직 모릅니다. 비교할 상대가 하나뿐이라 지금은 가릴 수 없습니다.

남은 것

  • 예제는 시험용입니다. 카드를 포맷하고, 출력을 통째로 덮어쓰고, 전화를 자동으로 거절합니다. 시험 과정에서는 맞는 동작이지만 제 기기에서는 사고여서, 핀 번호와 순서만 가져오고 예제 자체는 굽지 않는 편이 낫습니다.
  • 이름이 한 글자 다르면 다른 기기입니다. 문서가 틀린 것도 아니었고, 제가 읽던 문서의 기종이 달랐습니다. 최종 근거는 회로도입니다.
  • 재현이 되면 짐작하지 않습니다. 선을 흔들어 세 번 더 재현하고, 하나를 뽑아 비교까지 하고 나서야 원인이 끊김보다 잡음이라는 것을 알았습니다.
  • 세 배 차이가 나도 원인이 아닐 수 있습니다. 0.38ms 와 1.04ms 는 비율로는 크지만 손끝에서는 같습니다. 비율 대신 절대값을 봐야 했습니다.
  • 빨라지려고 고친 것이 보기를 해칠 수 있습니다. 14ms 와 750KB 를 얻고 페이드 효과를 잃었습니다. 되돌리는 방법을 같은 파일에 적어 두었습니다.

I am building a small panel to sit on a desk. An 800×480 board cycles iPhone notifications, calendar events, family photos, a clock and the temperature and humidity at that spot. The board is a Waveshare one, and it arrives with example code and documentation.

Nine commits over three days, and five of them undid something. All five were places where I had trusted the example or the documentation.

Flashing the SD example would have erased the photos

To read photos off the card I opened the vendor's SD example, meaning only to check the pin assignments. That example formats the card inside its test flow. Not as a format-if-mount-fails option: the call sits in the normal path.

The card held the original photos. I took the pin numbers without flashing it, turned the format option off in my own code, and limited writes to the photos folder.

The same example gave up one more. The function that turns on the backlight writes the IO expander's output as a whole byte, and the SD chip-select line lives in that same byte — so touching the backlight drops the card. A helper that changes one bit went in beside it.

The example's arithmetic was backwards

The notification side was ported from the example that ships with the development framework. Long messages were cut at 44 bytes, and I first took that for a limit on the iPhone's side.

The byte-shift for the high half of the attribute length ran the wrong way: << 8 where it should have been >> 8, so the ceiling of 300 I was asking for arrived as 44. That is 0x2C. Measured, 44 bytes of a 530-byte message came through.

After the fix it reaches 512 bytes. Asking for 1,000 still stops at 512, so that one really is the phone's limit. There were two limits, and the one I met first was in somebody else's code.

The documentation was for a different board

This board's name differs from the plain model by one letter. Copying the plain model's documentation got me two facts wrong: I wrote that there is no clock chip when there is one, and that the external connection is a connector when it is a screw terminal. Both came out only when I opened the schematic.

Everything is checked against this variant's schematic now. The documentation was not wrong — I was reading a different device's documentation.

Shaking a wire rebooted the board

Moving the one-metre temperature probe rebooted the board. Unwinding the backtrace gave an interrupt watchdog timeout, stopped inside the interrupt handler of the old I2C driver the example uses. I also checked that the firmware hash in the log matched the build I had just flashed.

Rather than guess a fix, I shook the wire on purpose. It stopped at the same place three more times within minutes. The plausible answer looking right until it is measured held here too. As a control I unplugged one wire entirely: sixteen failed reads and no hang. The cause was the noise while the wire moved, not the disconnection, with the old driver unable to leave the interrupt afterwards.

The bus now opens once, on the new driver, shared by the touch controller, the IO expander and the sensor. Mixing the two drivers hangs the boot, so not one old call remains — I checked the build output for the old file as well. After the fix, two shakes of the same strength: zero hangs, zero sensor failures out of 120.

The sluggishness was not the bus

Dragging the list felt sluggish. I had just dropped the bus to 100kHz for the sensor, so that is where I looked. Timing the touch reads gave 0.38ms at 400kHz against 1.04ms at 100kHz — nearly three times, which looks like a culprit.

I raised only the touch controller to 400kHz and asked again: it felt the same. 0.6ms is not a quantity a fingertip can feel. The sluggishness was the redraw, not the bus — another case of the culprit not being where I first pointed.

A draw-time monitor put the vendor's default anti-tearing mode at 77ms average per draw, against 63ms when drawing into 40-line bands in internal memory, with 750KB more external memory left over. I moved to the bands.

Something was lost for it. Drawing band by band makes a fade look like a staircase descending the screen, so the screen transitions and the photo's black curtain all came out. How to put them back is written in the config file's comments.

Not all of it was somebody else's code

One more thing rebooted on every reconnect, and that one was mine. The 1.2KB buffer holding an incoming notification was allocated on the Bluetooth task's 3KB stack. It moved to fixed memory with a lock around it. The same mistake came back three days later at 4KB, and it sits with five more silent failures in Thirteen Bytes Over, and the Layout Vanished.

What I still don't know

Bluetooth throughput differs from day to day. Same firmware, same spot, measured anywhere from 8.7KB/s to 60KB/s. The parameters requested at connection come back identical every time, and before requesting them it was 19KB/s. Cause undetermined. At the slow end a photo takes 7 to 12 seconds and a firmware replacement 6 to 10 minutes.

Set beside a household thermometer, the sensor agrees on temperature at 26.1°C and differs by 6% on humidity. Which one is right, I don't know yet. With only one instrument to compare against, there is no way to separate them.

What's left

  • Examples are written for testing. They format, they overwrite a whole output register, they auto-reject incoming calls. Correct inside a test flow, an accident on my device. Better to lift the pin numbers and the sequence and never flash them.
  • One letter of difference is a different device. The documentation was not wrong; I was reading the wrong documentation. The schematic is the final word.
  • When it reproduces, stop guessing. Three more reproductions from shaking the wire, plus a control with one wire pulled, was what showed the cause to be noise rather than disconnection.
  • A threefold difference can still be innocent. 0.38ms and 1.04ms are far apart as a ratio and identical to a fingertip. It needed reading as an absolute, not a ratio.
  • A fix for speed can cost you the look. I gained 14ms and 750KB and lost the fade. How to undo it sits in the same file.