2026-04-30

Bringing up a Cortex-M4 Buzzer on STM32MP153D: Lessons from the Pain

Bringing up a Cortex-M4 Buzzer on STM32MP153D: Lessons from the Pain

I just spent a long week trying to bring up a small Cortex-M4 firmware on an STM32MP153D board running OpenSTLinux. The firmware is simple in spirit. It plays sounds on a piezo buzzer using PWM from TIM1. The A7 cores keep running Linux. The M4 just plays beeps and short tunes.

That sounds easy. It was not easy. I want to write down the traps I fell into so other people do not lose the same week.

What I wanted

The STM32MP153D has two Cortex-A7 cores that run Linux, and one Cortex-M4 core for real-time work. I wanted to:

  • Build my own M4 firmware (Buzzer.elf).
  • Have U-Boot load it into the M4 at boot.
  • Let Linux just “attach” to the running M4 and talk to it later through RPMsg, which is a way for the two CPU cores to talk to each other.
  • Not break the rest of Linux.

The plan was clear. The pain was in the small details.

Trap 1: “rproc attached” means your reboot does nothing

On the Linux side I checked the M4 status:

cat /sys/class/remoteproc/remoteproc0/state

It said attached. Not running. Not offline. Just attached.

Here is what that really means. U-Boot loads the M4 firmware. Linux does not. Linux only attaches to whatever is already running on the M4. So when I did a normal reboot from Linux, only the A7 cores reset. The M4 kept running the OLD firmware. My new code never ran.

I wasted an entire debug session because of this. I rebuilt my firmware, copied it to /boot/rproc-m4-fw.elf, ran reboot, and tested. The bug was still there. Of course it was. The board was still running last week’s image.

The fix is to do a real hardware reboot:

echo 1 > /proc/sys/kernel/sysrq
sync
echo b > /proc/sysrq-trigger

echo b triggers a hardware-level reboot. Both the A7 cores and the M4 core reset. U-Boot runs again and loads the new ELF. Now you are testing what you think you are testing.

Lesson: if your remoteproc state says attached, treat reboot as a lie.

Trap 2: The ETZPC firewall is hiding behind everything

STM32MP1 has a peripheral firewall called ETZPC. It decides which world owns each peripheral: secure (TF-A / OP-TEE), Linux, or the M4. By default, almost nothing is given to the M4.

I wanted TIM1 for the PWM, plus DMA2 and DMAMUX1 to feed samples to TIM1. So I added them to the OP-TEE device tree as DECPROT_MCU_ISOLATION, which is the setting that gives the device to the M4 core:

DECPROT(STM32MP1_ETZPC_TIM1_ID,    DECPROT_MCU_ISOLATION, DECPROT_UNLOCK)
DECPROT(STM32MP1_ETZPC_DMA2_ID,    DECPROT_MCU_ISOLATION, DECPROT_UNLOCK)
DECPROT(STM32MP1_ETZPC_DMAMUX1_ID, DECPROT_MCU_ISOLATION, DECPROT_UNLOCK)

If you forget this step, the M4 firmware runs but its writes to TIM1 do nothing. No error message. No crash. Just silence. Or a hang, if you are unlucky and the bus parks on an unanswered access.

Lesson: when something on the M4 just “does not work,” check ETZPC before you check anything else.

Trap 3: Linux still tries to probe what you handed off

So I told ETZPC that TIM1 belongs to the M4. Done, right?

Boot. Hang.

The reason: Linux did not know about my decision. Its device tree still had &timers1 enabled. The PWM driver tried to probe TIM1, hit the firewall, and got -EACCES. That error then cascaded into a device-link hang during boot. The kernel sat there waiting for a device that would never come up.

The fix is short. Disable TIM1 on the Linux side too:

&timers1 {
    status = "disabled";
};

Both sides of the system have to agree about who owns each peripheral. The secure world hands it off, and Linux has to stop touching it. If only one side knows, you get a hang.

Lesson: every ETZPC handoff is two edits, not one.

Trap 4: Page alignment for OpenAMP carveouts

For RPMsg to work, you need shared memory carveouts. A carveout is a memory region we set aside that the kernel does not use for normal allocation. I needed four:

  • mcuram_rproc for the M4 firmware code itself.
  • vdev0vring0 and vdev0vring1 for the two queues in shared memory (a vring is a queue in shared memory, used by virtio).
  • vdev0buffer for the actual RPMsg message buffers.

My first try used the sizes from a sample:

vdev0vring0: vdev0vring0@10040000 { reg = <0x10040000 0x400>; ... };
vdev0vring1: vdev0vring1@10040400 { reg = <0x10040400 0x400>; ... };
vdev0buffer: vdev0buffer@10040800 { reg = <0x10040800 0x3000>; ... };

That is 1 KB for each vring and 12 KB for the buffer. Boot. The kernel rproc-virtio probe failed with -ENOMEM.

It took me a while to figure out why. The vring carveouts MUST be aligned to PAGE_SIZE (4 KB on ARM) AND sized in 4 KB multiples. 1 KB sizes are simply not allowed. The kernel cannot map a sub-page region as DMA-coherent memory.

The fix is to bump everything to 4 KB or more, and slide the addresses so each region starts on a 4 KB boundary:

vdev0vring0: vdev0vring0@10040000 { reg = <0x10040000 0x1000>; ... };
vdev0vring1: vdev0vring1@10041000 { reg = <0x10041000 0x1000>; ... };
vdev0buffer: vdev0buffer@10042000 { reg = <0x10042000 0x4000>; ... };

Lesson: every reserved-memory region for remoteproc must be page-aligned and page-sized. No exceptions.

Trap 5: The M4 ELF must use M4-side addresses

This one stung. The M4 sees its own memory through different addresses than Linux does. The chip has an AHB bridge that maps the M4 retention RAM at 0x38000000 from the A7 side, but the M4 itself only sees that same RAM starting at 0x00000000.

When I built the ELF, I had to make sure the vector table goes at 0x00000000, not 0x38000000. The dma-ranges in stm32mp151.dtsi map M4 0x00000000 to Linux 0x38000000, but that mapping is for DMA, not for the M4 program counter.

Get this wrong, and the M4 boots into garbage and never even fetches your reset handler. You will see nothing. No log. No life sign.

Lesson: in an asymmetric multi-processing system, you have to remember which side is reading which address.

Trap 6: How do I know my firmware is even running?

After all this, how do you actually know the M4 is running your new code?

md5sum /boot/rproc-m4-fw.elf is not enough. The file on disk can be new while the M4 still runs the old image (see Trap 1).

Here is the trick I ended up using. Pick a 4-byte word well inside the .text section. On the host, read it from the ELF file:

xxd -s 0x28fc -l 4 ssonic-m4-fw/STM32CubeIDE/CM4/Debug/Buzzer.elf

The math: my .text LMA is 0x10000000, and the ELF file places .text at file offset 0x2000. So file offset 0x28fc is .text + 0x8fc, which sits at runtime address 0x100008fc.

On the target, read that address from M4 memory using a small debug command I added:

m4dbg 0x100008fc

If the two values match, the M4 is running the new firmware. If not, do echo b > /proc/sysrq-trigger and try again.

Pick an address well inside .text, not near the start. The first kilobyte or so is mostly identical between builds (vector table, startup code), so it does not tell you anything.

Lesson: build a “fingerprint” check into your dev loop. You will save hours.

Closing thoughts

The buzzer is making sounds now. The MP3-to-PWM streaming side still has a producer/consumer race I am chasing, but at least I trust my dev loop today. A week ago I did not.

If I had to give one piece of advice to my past self, it would be this: do not trust reboot. That single misunderstanding was responsible for the most wasted hours.

The other lessons in short:

  • ETZPC handoff is two edits: OP-TEE DTS, and Linux DTS. Both sides must agree.
  • Vrings are page-aligned and page-sized. Always. No 1 KB shortcuts.
  • M4 sees its own memory at 0x00000000. Linux sees the same RAM at 0x38000000. Build the ELF for the M4’s view.
  • Always check the firmware fingerprint at runtime, not just on disk.

If you are starting an M4 project on STM32MP1, I hope this saves you a few of the days it cost me.

How to Send a Patch to Trusted Firmware-A (TF-A) using Gerrit

How to Send a Patch to Trusted Firmware-A (TF-A) using Gerrit

TF-A does not take patches by email like the Linux kernel. You must use Gerrit at https://review.trustedfirmware.org. This guide shows every step, from making an account to pushing your first patch. I tested it by sending a real BL2 patch.

1. Make a trustedfirmware.org account

  1. Open https://review.trustedfirmware.org/login.
  2. Sign in with GitHub, Google, or GitLab. There is no password sign-up.
  3. Pick your username at first login. It is hard to change later, so think before you choose.
  4. After login, click your profile and go to Settings → Profile. Set your Full name. This name shows up as the owner of every Change you make.

2. Add your SSH key

You push to Gerrit over SSH. Make a key if you do not have one:

ssh-keygen -t ed25519 -C "your-email@example.com"
cat ~/.ssh/id_ed25519.pub

Copy the public key. In Gerrit, go to Settings → SSH Keys → Add new SSH key, paste it, and click Add.

3. Set up SSH (optional but easier)

Add this to ~/.ssh/config so you do not have to type the host and port every time:

Host review.trustedfirmware.org
    User <your-gerrit-username>
    Port 29418
    IdentityFile ~/.ssh/id_ed25519

Test it:

ssh -p 29418 review.trustedfirmware.org gerrit version
# Should print: gerrit version 3.12...

4. Clone the repo and add the Gerrit remote

git clone https://git.trustedfirmware.org/TF-A/trusted-firmware-a.git
cd trusted-firmware-a

git remote add gerrit ssh://review.trustedfirmware.org:29418/TF-A/trusted-firmware-a

5. Install the commit-msg hook

Gerrit uses a Change-Id line in each commit to track the same review across new versions. The hook adds it for you:

curl -Lo .git/hooks/commit-msg https://review.trustedfirmware.org/tools/hooks/commit-msg
chmod +x .git/hooks/commit-msg

To add a Change-Id to a commit you already made:

git commit --amend --no-edit

6. Make your branch and write your patch

git fetch gerrit integration
git checkout -b my-fix gerrit/integration

# edit code...

git add -p
git commit -s   # -s adds Signed-off-by, which is required

Write the commit message in this style (TF-A uses conventional commits):

fix(st): reset SoC instead of panic() on MMC init failure

Explain why you made the change. Wrap lines at 80 columns.

Signed-off-by: XXX <XXX@XXX.XXX>
Change-Id: I0123456789abcdef0123456789abcdef01234567

Common types: feat, fix, refactor, docs, build, ci, style, test, chore. The scope is the folder or module, like st, cpus, tc.

7. Push to Gerrit

You do not push to a normal branch. You push to a magic ref called refs/for/<branch>:

git push gerrit HEAD:refs/for/integration

You will see something like:

remote: SUCCESS
remote:   https://review.trustedfirmware.org/c/TF-A/trusted-firmware-a/+/<NNNNN> fix(st): ... [NEW]

Open that URL. That is your Change. You can read the diff and reply to comments there.

8. Add reviewers

Click ADD REVIEWER in the web UI, or do it from SSH:

ssh -p 29418 review.trustedfirmware.org gerrit set-reviewers \
  --add reviewer1@example.com \
  --add reviewer2@example.com \
  -- <change-number>

Watch out: the email a maintainer uses for git commits may not be the email they used to sign up for Gerrit. For example, one ST maintainer commits as @foss.st.com but is registered on Gerrit as @st.com. If you get “Account not found”, try other domain forms.

9. Send a new version (v2)

Keep the same Change-Id and the new patch will go into the same review as Patch Set 2:

# edit code...
git commit --amend     # the Change-Id stays
git push gerrit HEAD:refs/for/integration

The hook keeps the same Change-Id when you amend. A new Change is not made.

10. Things that often go wrong

  • “missing Change-Id” — push refused → You did not install the commit-msg hook. Run git commit --amend after you install it.
  • “prohibited by Gerrit: not permitted: create” → You pushed to a normal branch. You must use refs/for/integration.
  • checkpatch fails → TF-A runs a Linux-style checkpatch.pl in CI. Check first with ./scripts/checkpatch.py --no-tree -f <file> (or use the Linux kernel checkpatch.pl).
  • Permission denied (publickey) → The SSH key on Gerrit and your local key do not match. Run ssh -vT review.trustedfirmware.org -p 29418 to see which key is used.
  • A maintainer asks you to send the patch by email → That should not happen. TF-A does not take email patches. Reply with your Gerrit URL.

I used these steps to push a real BL2 reset patch for an STM32MP-based board, from sending a mailing-list email, to moving to Gerrit, to adding reviewers — all in one flow.

2026-04-29

Linux gpio-74x164 에 lines-initial-states 지원 추가 — 74HC595 chain 의 부팅 글리치 잡기 (개정판)

74HC595 / 74LVC594 같은 직렬-입력 병렬-출력 시프트 레지스터를 SPI 로 cascade 해서 GPIO 32개를 만들어 쓰는 보드를 다뤄본 적이 있으면 부팅 직후 짧게 깜빡이는 LED 한 줄을 본 기억이 있을 것이다. active-low 인디케이터를 한 칩에 모아둔 보드라면 더 또렷하다 — 부팅이 시작되자마자 모든 LED 가 한 번 전부 켜졌다가 user space 가 default-state 를 적용하면서 사라진다.

문제 — 74HC595 의 power-on 출력이 정의되어 있지 않다

74HC595 패밀리는 push-pull 출력 전용이고 read-back 경로가 없다. 그래서 Linux 의 gpio-74x164 드라이버는 chain 전체의 상태를 자기 메모리(chip->buffer) 로만 추적하고, 누가 한 비트를 set 하면 chain 전체를 한꺼번에 SPI 로 다시 쓴다 (__gen_74x164_write_config()). 그런데 chip->buffer 는 probe 시점에 devm_kzalloc() 에서 갓 나온 0x00 으로 시작한다. 결과적으로 chain 의 첫 번째 SPI write 는 항상 모든 출력 = 0 이다. active-low LED 보드에서는 그게 곧 "전부 켬" 이다.

이 글리치는 user space 의 gpio-leds default-state 가 적용되기 전, 즉 부팅 후 보통 수십~수백 ms 동안 보인다. 잠깐이라 무시할 만해 보이지만, 보드가 양산 운영 상태로 올라올 때마다 매번 그렇다는 게 문제다. CCTV 표시등이라면 짧은 점등이 사용자 신호로 잡힐 여지가 있다.

왜 gpio-hog 로는 안 되나

익숙한 해결책으로 DT 의 gpio-hog 가 떠오른다. 그런데 이 보드 / 이 드라이버 조합에는 세 가지 이유로 맞지 않는다.

  1. consumer 와 배타적이다. 이 보드는 chain 의 모든 라인을 gpio-leds 가 이미 claim 하고 있다. gpio-hog 는 hog 가 라인을 claim 하므로 leds 와 동시 사용이 불가능하다.
  2. 순차 적용으로는 글리치를 막을 수 없다. hog 는 라인별로 한 번씩 set 호출이 들어간다. __gen_74x164_write_config() 가 매번 chain 전체를 다시 쓰므로, hog 4개라면 full-chain SPI write 4회. 첫 hog 가 set 되기 전 0x00 한 번이 무조건 먼저 나간다 — 글리치 자체는 그대로 남는다.
  3. 의도와도 다르다. gpio-hog 의 binding 문서는 "driver 가 claim 하지 않는 line" 용도로 못 박혀 있다. 이 chain 의 라인은 driver(gpio-74x164) + consumer(gpio-leds) 가 모두 claim 한다.

그러므로 글리치를 진짜로 없애려면, driver 가 chain 의 첫 SPI write 전에 buffer 를 의도한 패턴으로 미리 채워둬야 한다. 그리고 그 패턴은 보드별로 다르므로 device tree 가 자연스러운 선언 위치다.

v1 — 새 property 'registers-default' 도입

처음에 보낸 v1 시리즈는 새 property registers-default (uint8-array, chip 별 한 바이트씩) 를 binding 에 추가하고 driver 가 이를 읽어 buffer 에 채우는 방식이었다.

gpio-leds-spi {
    compatible = "fairchild,74hc595";
    registers = <4>;
    registers-default = /bits/ 8 <0xff 0xff 0xff 0xff>;
    /* ... */
};

주말 동안 checkpatch.pl --strict, yamllint, make dt_binding_check, dt-validate 까지 모두 깨끗히 통과시키고 scripts/get_maintainer.pl 결과대로 GPIO/DT-bindings maintainer 들에게 송신했다.

v1 review — 두 가지 지적과 더 좋은 길

Krzysztof Kozlowski 가 두 가지를 지적했다.

  1. 왜 이 드라이버만 DT 에서 default 가 필요한가 — driver 에서 zero 안 쓰든지, 아니면 hog 를 제대로 처리하든지 둘 중 하나로 가는 게 맞지 않나.
  2. registers-default 는 vendor-prefix 가 없는 generic property 다 — vendor-specific binding 에 generic property 를 추가하는 건 can of worms 의 시작이다.

1번에는 위에서 정리한 74HC595 의 하드웨어 특성 + gpio-hog 가 맞지 않는 세 가지 이유로 답장을 했다. 그런데 답장 직후 Linus Walleij 가 다른 angle 의 제안을 줬다 — 이미 비슷한 binding 이 존재한다. nxp,pcf8575lines-initial-states property 가 정확히 같은 의도(부팅 시 라인별 초기값)로 쓰이고 있고, generic 한 위치에 documented 되어 있다는 것. 그러면 굳이 새 property 를 만들 필요 없이 그것을 재사용하면 된다.

이 한 마디가 v1 review 의 두 지적을 한 번에 풀어준다.

  • Krzysztof 의 vendor-prefix 우려 → 이미 nxp,pcf8575 binding 에 documented 되어 있는 property 라 새로 만드는 것이 아니다.
  • Krzysztof 의 "왜 이 driver 만 필요한가" 우려 → 이미 다른 driver(pcf8575) 도 같은 패턴을 쓰고 있어 이 driver 만 특별한 것이 아니게 된다.

review 가 더 좋은 방향을 짚어준 사례다. 코드를 더 짧고 더 일반적인 쪽으로 끌어줬다.

v2 — lines-initial-states 재사용

v2 는 새 property 도입을 전부 들어내고, lines-initial-states bitmask 를 재사용하는 방향으로 다시 짰다.

Binding YAML — property 한 줄 추가

properties:
  ...
  lines-initial-states:
    description: |
      Bitmask specifying the boot-time output state of each GPIO line.
      Bit N corresponds to GPIO line N. Since this device is output-only,
      bit=0 drives the line low and bit=1 drives it high. Lines beyond
      the bitmask come up low.
    $ref: /schemas/types.yaml#/definitions/uint32

examples:
  - |
    gpio-leds-spi {
        compatible = "fairchild,74hc595";
        registers = <4>;
        lines-initial-states = <0xff00ff00>;
        /* line 0..7 low, line 8..15 high, line 16..23 low, line 24..31 high */
    };

Driver — probe 에 if 블록 하나 추가

@@ -112,7 +112,7 @@ static int gen_74x164_probe(struct spi_device *spi)
 {
     struct device *dev = &spi->dev;
     struct gen_74x164_chip *chip;
-    u32 nregs;
+    u32 nregs, init_state;
     int ret;

@@ -134,6 +134,21 @@ static int gen_74x164_probe(struct spi_device *spi)

     chip->registers = nregs;

+    /*
+     * Optionally seed the chain with a board-specified pattern so the
+     * outputs come up in a known state on the first SPI write. The
+     * property follows the nxp,pcf8575 convention where bit N maps to
+     * GPIO line N. On this output-only device, bit=0 drives the line
+     * low and bit=1 drives it high. The bitmask covers up to 32 lines;
+     * any further outputs come up zeroed by devm_kzalloc().
+     */
+    if (!device_property_read_u32(dev, "lines-initial-states", &init_state)) {
+        unsigned int i;
+
+        for (i = 0; i < min(nregs, 4U); i++)
+            chip->buffer[nregs - 1 - i] = (init_state >> (i * 8)) & 0xff;
+    }
+
     chip->gpiod_oe = devm_gpiod_get_optional(dev, "enable", GPIOD_OUT_LOW);

이 driver 의 buffer layout 은 마지막 chip 이 인덱스 0 에 오므로, bit-N=line-N 매핑을 유지하기 위해 buffer 인덱스를 뒤집어 채운다. 32 라인 이하 (4-chip cascade 까지) 는 곧바로 들어가고, 그 이상은 어차피 devm_kzalloc 의 0 으로 남는다. 이 보드도 32 라인 이하라 충분하다.

변경 크기

 .../bindings/gpio/fairchild,74hc595.yaml | 13 +++++++++++++
 drivers/gpio/gpio-74x164.c               | 17 ++++++++++++++++-
 2 files changed, 29 insertions(+), 1 deletion(-)

v1 (binding 22+ / driver 28+) 보다 v2 가 절반 정도다. 새 property 를 만들지 않고 기존 컨벤션을 따르니 binding 도 driver 도 줄었다.

v2 송신과 thread 유지

v2 는 --in-reply-to=<v1 cover Message-Id> 로 보내서 lkml 의 같은 thread 아래에 쌓이도록 했다. Suggested-by: Linus Walleij <linus.walleij@linaro.org> trailer 도 driver 패치에 추가했다.

git format-patch -v2 --cover-letter -2     --in-reply-to='cover.1776872453.git.happycpu@gmail.com'     -o ../outgoing
git send-email     --to='linusw@kernel.org' --to='brgl@kernel.org' ...
    ../outgoing/

v2 cover letter 의 "Changes since v1" 섹션에 위에 적은 두 review 지적 → lines-initial-states 재사용으로 한 번에 해결, 의 흐름을 짧게 정리했다. lore 에 v2 가 v1 cover 의 답신으로 잡혀 있으면 reviewer 들이 thread 를 따라가기 편하다.

마무리

이 시리즈는 단순한 코드 변경 (driver 16+ 줄, binding 13+ 줄) 인데도 review 사이클을 한 번 돌면서 더 일반적이고 더 짧은 구현으로 정리됐다. v1 의 "vendor-prefix 없는 generic property 추가" 는 통과시켜도 동작은 했겠지만, 비슷한 시나리오가 있을 다른 driver 들 (pcf8575 외에도) 이 각자 다른 이름으로 같은 의미의 property 를 만들면서 기능이 흩어졌을 것이다. v2 는 "기존에 있는 컨벤션 (lines-initial-states) 을 다른 family 의 binding 에도 documented 하기" 로 바뀌었고, 결과적으로 mainline 유지 비용이 더 적은 형태가 됐다.

upstream 송신은 패치를 받게 만드는 것이 목적이 아니라 여러 사용 사례에 잘 맞도록 정제하는 과정이라는 것을, 짧은 시리즈 한 번에 다시 확인했다.

이 변경이 mainline 에 들어오면 이 보드의 다운스트림 패치 (0002-gpio-74x164-registers-default.patch) 도 자동으로 drop 가능하다. 이 보드 외에도 비슷한 cascade 보드가 이 패치를 활용할 수 있도록, mainline 단 한 줄짜리 documented 컨벤션으로 정리되는 편이 모두에게 가성비가 좋다.

TF-A 의 eMMC 초기화 실패 시 panic 대신 SoC 리셋으로 자가 복구하기

STM32MP153D 보드를 eMMC 부팅으로 양산하다 보면 가끔 부팅 중 멈춰 있는 보드를 만난다. 시리얼 콘솔에는 이게 마지막 메시지다.

SDMMC2 init failed
PANIC at PC : ...

그리고 그대로 영원히 정지. 외부 전원을 끊어 다시 넣어주면 멀쩡히 살아나는 경우가 대부분인데, 이건 일과성 실패 — 전원 레일 ramp 타이밍, eMMC fast-boot 모드 진입 직후의 버스 라인 노이즈, RCC 클록 도메인 안정화 jitter 같은, cold boot 한 번이면 사라지는 부류의 문제다. 그런데 TF-A BL2 는 그 한 번의 cold boot 기회를 스스로 만들지 못하고 panic() 에 갇혀 사람이 와서 전원을 뽑아주기를 기다린다. 양산 환경에서는 이게 사고가 된다.

왜 panic 보다 reset 인가

일과성 실패의 복구 경로는 결국 cold boot 한 번이다. 그러면 BL2 가 panic() 으로 SoC 를 정지시키는 대신 stm32mp_system_reset() 으로 SoC 자체를 reset 시켜버리는 편이 자연스럽다. RCC 시스템 리셋이 들어가면 BootROM 이 다시 실행되어 BL2 도 처음부터 다시 unpack/실행되니, 일시적 잡음으로 인한 init 실패는 다음 부팅 cycle 에서 자동으로 해소된다. 사람이 안 와도 된다.

패치 내용

변경 위치는 한 곳이다 — plat/st/common/bl2_io_storage.cboot_mmc(). stm32_sdmmc2_mmc_init() 가 실패한 직후 panic() 직전에 reset 호출 한 줄과 짧은 주석을 끼워 넣는다.

변경 전

params.device_info = &mmc_info;
if (stm32_sdmmc2_mmc_init(&params) != 0) {
    ERROR("SDMMC%u init failed\n", boot_interface_instance);
    panic();
}

변경 후

params.device_info = &mmc_info;
if (stm32_sdmmc2_mmc_init(&params) != 0) {
    ERROR("SDMMC%u init failed - resetting system\n",
          boot_interface_instance);
    /*
     * eMMC init failures here are usually transient (rail-ramp
     * timing, bus-line noise on fast-boot entry, RCC clock-domain
     * settling jitter). panic() leaves the SoC frozen and forces
     * an external power cycle; a system reset lets BootROM re-run
     * the entire boot path, which most transient failures survive.
     * stm32mp_system_reset() is __dead2, so panic() below is a
     * defensive fallback if the reset circuit is itself wedged,
     * and a no-return marker for analyzers.
     */
    stm32mp_system_reset();
    panic();
}

그리고 reset API 헤더 한 줄 추가.

 #include <drivers/st/stm32_sdmmc2.h>
+#include <drivers/st/stm32mp_reset.h>
 #include <drivers/usb_device.h>

전체 diff 는 한 파일에서 14줄 추가, 1줄 삭제. 정말 한 줄 변경이라고 봐도 무방하다.

panic() 도 그대로 두는 이유

stm32mp_system_reset() 은 헤더에 __dead2 (no-return) attribute 가 달려 있어, 호출 즉시 SoC 가 리셋되고 그 다음 줄의 panic() 에는 정상 동작에서는 절대 도달하지 않는다. 그런데 일부러 panic() 호출을 지우지 않고 그대로 둔 데는 두 가지 이유가 있다.

  1. 방어적 fallback. 리셋 회로나 전원 시퀀서에 결함이 있어 stm32mp_system_reset() 이 실제로는 리셋을 못 일으키는 만일의 상황을 가정해보자. 이때는 그래도 panic() 루프에 머물러 있는 편이, 실패 지점 너머로 실행을 계속하는 것보다 훨씬 안전하다. 절대 정지하면 안 되는 곳에서 정지하는 것보다, 절대 진행하면 안 되는 곳에서 진행하는 게 더 위험하다.
  2. 정적 분석기와 후속 수정자에 대한 명시적 표식. 이 분기는 "절대 그 이상 진행해서는 안 된다" 는 의미를 코드에 박아두는 것이다. 누군가 나중에 코드를 손보면서 reset 호출만 지우고 정책을 슬그머니 되돌리는 일을 막는다.

이런 방어선은 비용이 거의 없으면서 (panic() 한 줄) 코드 의도를 명확히 남긴다는 점에서 가성비가 좋다.

다운스트림 v2.4 → mainline v2.10 포팅 노트

본 변경은 사실 STM32MP153D 보드에서 TF-A v2.4 백포트로 1년 넘게 양산 운영해온 정책이다. v2.4 다운스트림 패치 (0005-mmc-init-reset.patch) 에는 함께 들어있는 두 가지가 더 있는데, mainline 송신본에서는 의도적으로 뺐다.

  • MMC_DEFAULT_MAX_RETRIES 를 5에서 50으로 늘리는 변경. 무조건 reset 으로 떨어지는 fallback 이 있는 이상 retry 횟수를 더 늘릴 동기가 약하고, 한 부팅 안에서 50번 재시도하느니 한 번 reset 하고 cold boot 부터 깔끔히 다시 가는 편이 결과적으로 빠르다.
  • "SDMMC init1 start / done" NOTICE 디버그 로그. production BL2 콘솔을 가능한 한 조용하게 유지하기 위해 뺐다. 디버깅이 필요하면 별도 NOTICE_LEVEL 로 켜는 편이 깔끔.

또 v2.4 에는 stm32image 포맷용 별도 파일 plat/st/common/bl2_stm32_io_storage.c 에도 동일한 panic() 위치가 있었는데, v2.10 에서는 이 파일이 bl2_io_storage.c 로 통합되어 패치 대상이 한 곳뿐이다. 통합된 김에 한 hunk 로 두 boot flow 를 모두 커버한다.

upstream 송신 결과

2026-04-29 03:21 UTC, TF-A 메일링 리스트로 송신했다.

  • Subject: [PATCH] fix(st): reset SoC instead of panic() on MMC init failure
  • Message-Id: <20260429032109.789181-1-happycpu@gmail.com>
  • To: tf-a@lists.trustedfirmware.org, yann.gautier@foss.st.com (ST plat maintainer)
  • Cc: madhukar.pappireddy@arm.com, manish.badarkhe@arm.com, olivier.deprez@arm.com (TF-A maintainers)
  • Archive: lists.trustedfirmware.org/archives/list/tf-a@lists.trustedfirmware.org/

TF-A 는 lore.kernel.org 가 아닌 hyperkitty 에 메일링 아카이브가 인덱싱되고, 실제 merge 채널은 메일링이 아니라 review.trustedfirmware.org Gerrit 이다. 메일링은 정책 RFC 토론 용도. ST plat maintainer ack 받으면 같은 commit 을 Gerrit 에 push 해서 정식 review 트랙으로 넘길 예정이다.

마무리

한 줄짜리 변경이지만 운영 입장에서 의미가 크다. "보드를 손으로 가서 전원을 뽑아야 살아나는 사고" 가 "다음 cold boot 한 번이면 자동 복구되는 일과성 잡음" 으로 바뀐다. 그동안 같은 경험을 모은 보드가 한두 대가 아니라면 이 변경은 사고 수를 통째로 한 자리수씩 깎아준다.

비슷한 위치에서 panic() 으로 정지하는 부트로더/펌웨어 코드는 가능한 한 reset 으로 떨어뜨려두는 정책이 양산 환경에 맞다. __dead2 함수 호출 + panic() fallback 의 두 줄짜리 패턴은 어디서 써도 가성비가 좋다.

2025-06-02

gitea 설치

깃 서버

wget -O gitea https://dl.gitea.com/gitea/1.22.0/gitea-1.22.0-linux-amd64
chmod a+x gitea
./gitea web

github 에서 미러링시에, access token을 넣어야하는데,
access token생성시  repo 항목 모두 채크하고 만들어야 땡겨짐.


2025-05-25

와이파이핫스팟 인터넷 활성화.wifi hotsopt internet enable

우분투22.04에서 와이파이핫스팟을 했는데. 연결된 클라이언트들이 인터넷이 안된다.
이유를 gpt에 물어봤더니. 보안상 masquerade와 forwarding을 drop시켰다.
이것들을 동작시 활성화 하도록 스크립트를 추가한다.

sudo mkdir -p /etc/NetworkManager/dispatcher.d

sudo vi /etc/NetworkManager/dispatcher.d/90-hotspot-nat.sh
#!/bin/bash

IFACE="$1"
STATUS="$2"

# 핫스팟 인터페이스명(수정 필요할 수도 있음)
HOTSPOT_IF="wlp0s20f3"

# 유선인터페이스 자동 탐색
eth_if=$(ip -o link show | awk -F': ' '{print $2}' | grep -E '^en' | grep -v 'br' | grep -v 'docker' | grep -v 'veth' | grep -v 'lo' | head -n1)

if [ "$IFACE" = "$HOTSPOT_IF" ] && [ "$STATUS" = "up" ]; then
    # NAT 추가 (중복 X)
    if ! iptables -t nat -C POSTROUTING -s 10.42.0.0/24 -o $eth_if -j MASQUERADE 2>/dev/null; then
        iptables -t nat -A POSTROUTING -s 10.42.0.0/24 -o $eth_if -j MASQUERADE
    fi
    # FORWARD 체인 정책 허용
    iptables -P FORWARD ACCEPT
fi

nmcli con modify Hotspot wifi-sec.key-mgmt wpa-psk
nmcli con modify Hotspot wifi-sec.pmf default
#nmcli con down Hotspot && nmcli con up Hotspot

sudo chmod +x /etc/NetworkManager/dispatcher.d/90-hotspot-nat.sh

2025-04-25

[siwg917] 공유기 mac 얻기, arp table에서 확인.



1. arp 요청함수.

#include "lwip/netif.h"
#include "lwip/ip4_addr.h"
#include "lwip/etharp.h"

#define ARP_RETRY_COUNT 10
#define ARP_WAIT_MS 100

void request_and_print_gateway_mac(const sl_net_wifi_client_profile_t *profile, struct netif *netif)
{
ip4_addr_t gateway_ip;
IP4_ADDR(&gateway_ip,
profile->ip.ip.v4.gateway.bytes[0],
profile->ip.ip.v4.gateway.bytes[1],
profile->ip.ip.v4.gateway.bytes[2],
profile->ip.ip.v4.gateway.bytes[3]);

// 1. ARP 요청 전송
if (etharp_request(netif, &gateway_ip) != ERR_OK) {
printf("Failed to send ARP request to gateway\r\n");
return;
}

// 2. ARP 응답 대기 및 MAC 주소 찾기
struct eth_addr *eth_ret;
const ip4_addr_t *resolved_ip;

for (int i = 0; i < ARP_RETRY_COUNT; i++) {
if (etharp_find_addr(netif, &gateway_ip, &eth_ret, &resolved_ip) >= 0) {
printf("Gateway MAC: %02X:%02X:%02X:%02X:%02X:%02X\r\n",
eth_ret->addr[0], eth_ret->addr[1], eth_ret->addr[2],
eth_ret->addr[3], eth_ret->addr[4], eth_ret->addr[5]);
return;
}
osDelay(100); // FreeRTOS 기반 대기
}

printf("Gateway MAC: [Not found in ARP cache after ARP request]\r\n");
}


2. 업데이트된 arp테이블에서 mac 보기. (함수일부)
void display_client_network_info(sl_net_wifi_client_profile_t profile)
{

printf("\r\n=== Wi-Fi Client Network Information ===\r\n");
printf("SSID: %s\r\n", profile.config.ssid.value);
printf("Security: %d\r\n", profile.config.security);

// IP 정보 표시
printf("\r\n--- IP Configuration ---\r\n");
printf("IP Management Mode: %s\r\n",
profile.ip.mode == SL_IP_MANAGEMENT_DHCP ? "DHCP" :
profile.ip.mode == SL_IP_MANAGEMENT_STATIC_IP ? "Static IP" : "Link Local");

if (profile.ip.type & SL_IPV4) {
printf("IPv4 Address: %d.%d.%d.%d\r\n",
profile.ip.ip.v4.ip_address.bytes[0],
profile.ip.ip.v4.ip_address.bytes[1],
profile.ip.ip.v4.ip_address.bytes[2],
profile.ip.ip.v4.ip_address.bytes[3]);

printf("Subnet Mask: %d.%d.%d.%d\r\n",
profile.ip.ip.v4.netmask.bytes[0],
profile.ip.ip.v4.netmask.bytes[1],
profile.ip.ip.v4.netmask.bytes[2],
profile.ip.ip.v4.netmask.bytes[3]);

printf("Gateway: %d.%d.%d.%d\r\n",
profile.ip.ip.v4.gateway.bytes[0],
profile.ip.ip.v4.gateway.bytes[1],
profile.ip.ip.v4.gateway.bytes[2],
profile.ip.ip.v4.gateway.bytes[3]);

// Gateway MAC 주소 표시
struct netif *netif = netif_default; // 또는 Wi-Fi에 해당하는 netif 지정
ip4_addr_t gateway_ip;
struct eth_addr *eth_ret;
const ip4_addr_t *resolved_ip;

IP4_ADDR(&gateway_ip,
profile.ip.ip.v4.gateway.bytes[0],
profile.ip.ip.v4.gateway.bytes[1],
profile.ip.ip.v4.gateway.bytes[2],
profile.ip.ip.v4.gateway.bytes[3]);

if (etharp_find_addr(netif, &gateway_ip, &eth_ret, &resolved_ip) >= 0) {
printf("Gateway MAC: %02X:%02X:%02X:%02X:%02X:%02X\r\n",
eth_ret->addr[0], eth_ret->addr[1], eth_ret->addr[2],
eth_ret->addr[3], eth_ret->addr[4], eth_ret->addr[5]);
} else {
printf("Gateway MAC: [Not found in ARP cache]\r\n");
}
}
}

3. 실행위치
static void application_start(void *argument)
{
UNUSED_PARAMETER(argument);
sl_status_t status;
sl_ip_address_t ip_address = { 0 };
sl_net_wifi_client_profile_t profile = { 0 };
sl_wifi_performance_profile_t performance_profile = { .profile = ASSOCIATED_POWER_SAVE_LOW_LATENCY };
status = sl_net_init(SL_NET_WIFI_CLIENT_INTERFACE, &client_configuration, &wifi_client_context, NULL);
if (status != SL_STATUS_OK) {
printf("Failed to start Wi-Fi Client interface: 0x%lx\r\n", status);
return;
}
printf("Wi-Fi Client interface success\r\n");

#ifdef SLI_SI91X_MCU_INTERFACE
uint8_t xtal_enable = 1;
status = sl_si91x_m4_ta_secure_handshake(SL_SI91X_ENABLE_XTAL, 1, &xtal_enable, 0, NULL);
if (status != SL_STATUS_OK) {
printf("Failed to bring m4_ta_secure_handshake: 0x%lx\r\n", status);
return;
}
printf("m4_ta_secure_handshake Success\r\n");

2025-02-02

[qemu] 실행

DISTRO=nodistro MACHINE=qemuarm source layers/meta-st/scripts/envsetup.sh

runqemu qemuarm

다음에는 간단한 distro를 만들어서 해봐야겠다.

2024-10-10

구글드라이브 wget 다운받는 명령어

구글드라이브의 파일을 링크가있는사람 무조건받기로 해서 공유링크를 복사하여 아래의 "구글드라이브공유링크"에 붙여넣는다. 원하는파일명은적절히 써준다. 

주의: 실행속성은 추가로 변경해주어야한다. chmod a+x 파일명

GDRIVE_LINK="구글드라이브공유링크" && FILE_ID=$(echo $GDRIVE_LINK | sed -n 's#.*d/\([^/]*\)/.*#\1#p') && wget --no-check-certificate "https://drive.google.com/uc?export=download&id=${FILE_ID}" -O 원하는파일명


Bringing up a Cortex-M4 Buzzer on STM32MP153D: Lessons from the Pain

Bringing up a Cortex-M4 Buzzer on STM32MP153D: Lessons from the Pain I just spent a long week trying to bring up a small Cortex-M4 firmware...