LoginSignup
4
3

More than 3 years have passed since last update.

armでOS超入門の簡易OSをarm-none-eabi-gccで開発してハマったこと

Last updated at Posted at 2019-10-20

統合開発環境用の簡易OSソースだけど少し修整したらgccで開発できると思ったのだが

ハマったこと一覧

  • インラインアセンブラからC言語変数にアクセスするとコンパイルエラー
  • 拡張インラインアセンブラからC言語の配列へのアクセス
  • 拡張インラインアセンブラのクロバーリストの挙動
  • Systickハンドラで更新するグローバル変数のtickが更新されない
  • cygwin gcc-armがコンパイルしたアセンブラソースが人が読めない出力
  • naked指定の関数はC言語とインラインアセンブラを混ぜると動作不定

はじめに

書籍「armでOS超入門」の簡易マルチタスクOSのソース(LPC Express用)が、次の点で実装が容易に思えたので、STM32VLdiscoveryボードで動かそうと思った。

  • 例外ハンドラをC言語で実装
  • 例外ハンドラでアセンブラが必要な処理はインラインアセンブラで実装
  • インラインアセンブラのソースからC言語の変数をアクセスする時、extern宣言や、_を変数の先頭に付けたりせずにアクセスしてる
  • 動作確認のデモタスクがLEDの点滅のみ(UARTを使わない)

非力なwin8.1上で統合開発環境を使うことはパスして、cygwinとarm社配布のgcc-arm (arm-none-eabi-gcc)を使うことにした。ボードのSTM-linkV1とGDBの連携環境の構築が面倒で、デバッガなしで開発したので色々ハマりました。。
ハマって変更した部分のソースは記述するが、書籍「armでOS超入門」のソースは記載しない。よって、この投稿だけでは動作するソースにはならない。

環境とMakefile

  • ボード STM32VLdiscovery
  • OS Window8.1 32bitメモリ1G
  • Cygwin 2.893
  • arm社配布のgcc-arm: arm-none-eabi-gcc.exe (GNU Tools for ARM Embedded Processors) 5.4.1 20160919 (release) [ARM/embedded-5-branch revision 240496]

Makefileはgcc-arm付属のサンプルを改造した。スタートアップアセンブラと、リンカスクリプトを書き直す必要があるが、STM32VLdiscoveryボード用のサンプルのスタートアップアセンブラとリンカスクリプトをそのまま使うことにした。
Makefileは最後の章に記載している。インクルードしてるmakefile.confもgcc-armサンプルのファイルを修正したものである。

インラインアセンブラからC言語変数にアクセスするとコンパイルエラー

書籍のOSソースのSVCハンドラやPendSVハンドラはC言語で記述していて、レジスタやスタック操作が必要な処理はインラインアセンブラで記述していた。拡張インラインアセンブラではないので、C言語の変数は、変数名をそのまま記述していた。gcc-armでは、そんな変数は存在しないでエラーとなった。

インラインアセンブラソース例
uint_t c_variable;
asm(
"movw r1, #:lower16:c_variable¥n¥t"
 ...
);

統合開発環境のコンパイラだとインラインアセンブラからC言語の変数名でアクセスできて正常に動作するらしい。。凄いお手軽だ。
gccでは拡張インラインアセンブラでC言語の変数にアクセスする。試していないが、externで宣言するとC言語変数名でアクセスできるかもしれない。出来たとしても、コンパイラがレジスタ割り当てを勝手に変更してしまい、正常に動作しないことが発生する。この不整合をgccで防ぐには、拡張インラインアセンブラで記述する必要がある。
ついでにインラインアセンブラはコンパイラが処理される順番を変更したり、インラインアセンブラを消して、同等の処理に書き換えることがある。これを防ぐには asm volatile (); と記述する。知らなければハマってしまう仕様だ。
拡張インラインアセンブラに書き直してコンパイルは正常終了した。正しく動くか不安だったので、拡張インラインアセンブラの部分をコピペしたプログラムをraspberry pi zeroでコンパイル-実行して確認することにした。
拡張インラインアセンブラに変更する時の参考に、動作確認したソースを記載しておく。

変数の読み書き。"=r"の=は書き込み、"+r"の+は読み書き、"r"の指定なしは読み込み。読み書きは、読み込みを先にすること。逆の時は指定が異なる

#include <stdio.h>
int main (int argc, char *argv[]) {
  unsigned int a = 1, b = 3, c = 6;
  printf("before a=%d, b=%d, c=%d\n", a, b, c);
  asm volatile (
      "mov r0, %[y]\n\t"
      "add r0, %[z]\n\t"
      "mov %[y], r0\n\t"
      "mov %[x], %[z]\n\t"
      : [x] "=r" (a), [y] "+r" (b)
      : [z] "r" (c)
      : "r0", "cc"
  );
  printf("after a=%d, b=%d, c=%d\n", a, b, c);
}
実行結果
before a=1, b=3, c=6
after a=6, b=9, c=6

構造体のメンバへの書き込み。インラインアセンブラでの配列へのレジスタ間接とオフセットの指定方法が解らなかった。
ポインタを介したアクセスで動作したので、まずは、コンパイル完して動作確認することにした。
配列のアクセスは次章に記述する。

#include <stdio.h>
typedef struct TSTK_STRUC {
  unsigned int r_r4;
  // 省略 スタックに積まれるレジスタ
} TSTK;

// TCB(Task Control Block)の定義
typedef struct TCTRL_STRRUC {
  unsigned char link;
  unsigned char state;
  TSTK          *sp;
  // 省略
} TCTRL;

static volatile TCTRL tcb[3] = {0};
static volatile TCTRL *c_task;

int main (int argc, char *argv[]) {
  c_task = &tcb[1];  // ポインタを介さない方法は次章で説明
  asm volatile (
    "mov  r12,#192\n\t"
    "mov %[a],r12\n\t"  // *(ctask-sp) = R12
    :[a] "=r" ((TSTK*)(c_task->sp))
    :
    :"r12"
  );
   printf("after c_task->sp=0x%x\n", tcb[1].sp);
}
実行結果
after c_task->sp=0xc0

C言語の変数にレジスタを割り当てて、拡張インラインアセンブラで、割り当てたレジスタ(変数)を読み書きする。結果をC言語変数で取得する。

#include <stdio.h>
int main(void) {
  register unsigned char c asm("r0");
  c = 0x7;
  asm volatile (
    "lsl r0, #3\n\t"
    ::
    : "r0", "memory", "cc"
  );
  printf("c=0x%x\n", c);
}
実行結果
c=0x38

関数呼び出し。arm社が配布しているgcc-arm (arm-none-eabi-gcc)は関数名でコールできない。GNUのlinux-gccだと関数名でコールできるが、%[a]とかではコールできない結果となった。

#include <stdio.h>
typedef struct TSTK_STRUC {
  unsigned int r_r4;
  unsigned int r_r5;
  // スタックに積むレジスタ-以降省略
} TSTK;

// TCB(Task Control Block)の定義
typedef struct TCTRL_STRRUC {
  unsigned char link;
  unsigned char state;
  TSTK          *sp;
  // 以降省略
} TCTRL;

static volatile TCTRL tcb[3] = {0};

void schedule(void) {
    tcb[3].sp = (TSTK*)0xcafe;
}

int main (int argc, char *argv[]) {
   tcb[3].sp = (TSTK*)0x1010;
   printf("before tcb.sp=0x%x\n", tcb[3].sp);
   asm volatile (
    "push {lr}\n\t"
    "bl %[b]\n\t"
    "pop {lr}\n\t"
    ::[b] "g"(schedule)
  );
  printf("after tcb.sp=0x%x\n", tcb[3].sp);
}
実行結果
before tcb.sp=0x1010
after tcb.sp=0xcafe

拡張インラインアセンブラからC言語の配列へのアクセス

前章では拡張インラインアセンブラでの配列へのアクセス方法が解らず、ポインタを介して拡張インラインアセンブラから参照する方法にした。
この方法で実装していくと、拡張インラインアセンブラの間接レジスタとオフセットの配列にアクセスするコードを書き直す必要が発生した。これが面倒になってきた。
ROM化できればソースの変更も少なくSTM32VLDiscoveryボードで動作すると思ったのが、実装を始めた動機だった。
アセンブラの配列のアクセスを全て書き換えていたら、実装を始めた動機に大きく矛盾する!
考えを悔い改めで、拡張インラインアセンブラの配列のアクセスを調べて判明した。次のテストコードで動作することを確認して、拡張インラインアセンブラの配列のアクセスも書き直す必要がなくなった。

配列への間接レジスタとオフセットによるアクセス。q_msgblkは配列データの先頭位置。q_msgblkの位置の配列データのlinkを無効置0xffにする。q_msgblkの位置の配列データを処理したので、q_msgblkを無効置0xffに設定する。

#include <stdio.h>
typedef struct MSGBLK_STRUC {
  unsigned char link;
  unsigned char  param_c;
  unsigned short param_s;
  unsigned int   param_i;
} MSGBLK;

unsigned char q_msgblk;
MSGBLK msgblk[8] = {0};

int main (int argc, char *argv[]) {
  unsigned char  saveVal = 0;
  q_msgblk = 2;
  saveVal = q_msgblk;
  printf("before q_msgblk =%d\n", q_msgblk );
  printf("before  msgblk[%d].link=0x%x\n", q_msgblk, msgblk[saveVal].link);
  asm  volatile (
    "mov r0,%[d]\n\t"
     "mov r2,%[e]\n\t"
     "mov  r3,#0xff\n\t"
     "strb r3,[r2,r0, lsl #3]\n\t"
     "mov %[d], r3\n\t"
     : [d] "+r"(q_msgblk)
     : [e] "r"(&msgblk) 
     : "r0","r2","r3"
  );
  printf("after q_msgblk =%d\n", q_msgblk );
  printf("after  msgblk[%d].link=0x%x\n", saveVal , msgblk[saveVal].link);
}
実行結果
before q_msgblk =2
before  msgblk[2].link=0x0
after q_msgblk =255
after  msgblk[2].link=0xff

拡張インラインアセンブラのクロバーリストの挙動

クロバーリスト(clobber list)は、インラインアセンブラで変更したレジスタを基本的に記述する。その他にccやmemoryもあり、memoryは、次のように説明されている。

memory is a valid keyword too.
It tells the compiler that the assembler instruction may change memory locations.

memory locationの正確な意味も解らず、メモリ更新するインラインアセンブラにmemoryを追加していくと正常に動作したインラインアセンブラがあった。
しかし、インラインアセンブラからC言語関数を呼び出し、呼び出されたC言語関数がタスク管理領域を更新するのは、memoryを指定したことによって正常に動かなくなった。理由は調べてないが、C言語関数を呼び出す時は、memory指定は注意が必要らしい。

Systickハンドラで更新するグローバル変数のtickが更新されない

なんとかコンパイルが出来て、ファームをボードに焼きこんで動作確認した。簡易OSは、tickが充分にカウントアップしたことをled点滅で知らせてから、タスクを開始させる。ボードのledを見ると、光る(tick確認開始)
のだが消灯(tickが充分に発火)しない。tickハンドラ待ちの無限ループになって停止したように見えた。

Tick部分のソース
static uint32_t systick_count;
void SysTick_Handler(void) {
  --systick_count;
  ……
}
main() {
  //Systick設定
 ……
  led点灯
  for (i=0; i<10; ++i)
    systick_count = 2;
    while(systick_count);
  }
  led消灯
  // タスク登録等
  ……
}

初めは、スタートアップアセンブラでSysTick_Handlerをweak指定してるので関数名の誤記かと思ったが、調べたらリンクはされていた。
デバッガ環境を構築しなかったのでSysTick_Handler内で別のledを点灯させて発火しているのを確認した。SysTick_Handlerでsystick_countを減らしているのに、main()ではsystick_countが減っていないことになる。(デバッガ環境を構築しなかったのを後悔した)
これはボードのサンプルを見て解決した。サンプルではsystick_countをvolatile※で修飾してた。※__IOマクロ
staticのグローバル変数であってもコンパイラはスタック上に変数の領域を確保する。すると例外ハンドラとタスクで別の領域の変数を参照することになってsystick_countが減らないのが発生してた。
例外ハンドラとタスクの両方で参照する変数はvolatileで修飾する必要がある。volatileで修飾するとスタックに領域を確保しなくなる。統合開発環境は、これも自動で設定するらしい?便利すぎる。

cygwin gcc-armがコンパイルしたアセンブラソースが人が読めない出力

簡易マルチタスクのソースのSVC_Handlerの先頭部分で、lrではなくr7を使ってるのでPushしてる処理があった。
環境特有の処理だと思い、アセンブラコードを確認しようと-Sで出力した。
ファイルを見ると文字ではなく、16進数文字コードで出力されており、そのまま読めるものではなかった。
デバッガ環境を構築しなかったことを再び後悔した。。

naked指定の関数はC言語とインラインアセンブラを混ぜると動作不定

なんとか動作確認用のLED点滅とSLEEPを繰り返すタスクが2個同時に動作した。
書籍に記載されているタスクを動作させたら、正常に動作しなかった。デバックコードを入れる毎に挙動が変化する動作不定の状態になった

タスク1
while(1) {
  タスク2起動コール
  フリーのMSGブロック取得
  MSGブロックにLED点灯を設定しタスク2に送信
  SLEEP(2)
  フリーのMSGブロック取得
  MSGブロックにLED消灯を設定しタスク2に送信
  SLEEP(2)
}
タスク2
whie(1) {
  IDLE状態遷移
  msg = MSG受信(WAIT_FOREVER)
  switch (MSGブロックのLEDフラグ) {
  case LED消灯:
    LED消灯サービスコール
    break;
  case LED点灯:
    LED点灯サービスコール
    break;
  }
  MSGブロックFREE(msg)
}

Sleepのみのタスク2個では常に正常に動作すること、上記のタスクではデバッグコードの入れ方によって挙動が変化することと、SVC_Hadlerがnaked指定していることから、ハードで自動退避されないレジスタが、想定外の上書きされていると推定。
書籍の統合開発環境用のSVC_Handlerは次のように実装されている。統合開発環境ではレジスタの上書きなしに正常に動作するらしい?

void SVC_Handler(void) __attribute__ ((naked));
void SVC_Handler(void) {
  asm (
    サービスコール番号と引数の保存
  );

  switch (サービスコール番号) {
  case XXXX:
    C言語のみの処理
    break;
  case XXYY:
    asm (
    );
    break;
  case YYYY:
  …… サービスコールの数のcase句
  ……
  }
  asm ("bx lr\n\t");
}

naked指定に関して調べていたら、(たぶん)armのコンパイラのリファレンスに次のような説明があった。
naked指定の関数はC/C++言語と拡張インラインアセンブラを混ぜて記述することはできない
うーん、混ぜて記述したから不定動作になったのか、SVC_Handerを全部アセンブラで書き直すか。
しかし、SVC_Handerをアセンブラで記述する必要がないから実装しようと思ったのに、本末転倒になるからアセンブラで書き直しは辞めた。
PendSV_Handerもnaked指定してるのに正常に動作しているのに気づいた。実装を確認したら、次のようにC言語の関数を呼びだして処理をしていた。
SVC_Handlerは拡張インラインアセンブラが多数あり、C言語変数の受け渡しも多数ある。
その為、自動退避されるレジスタでは足りずに、退避されないレジスタを使用していると推測。

void PendSV_Handler(void) __attribute__ ((naked));
void PendSV_Handler(void) {
  asm volatile (
    レジスタ退避
  );
  asm volatile (
    "push {lr}\n\t"
    "bl %[b]\n\t"
    "pop {lr}"
    ::[b] "g"(schedule)
  ); // scheduleはC言語関数
  asm volatile (
    レジスタ復帰
  );

SVC_HandlerもPendSV_Handlerのように書き換える方針に決定した。
サービスコールの番号や引数は、SVC_HandlerからコールするC言語関数に引数で渡すことにする。
次のような構造に変更する。

void SVCExec(uint32_t svcparams, uint32_t current_psp, uint32_t svcop) {
  2個の引数を一つにしたsvcparamsを2個の引数に戻す

  SVC_Handlerの処理をコピー
}
void SVC_Handler(void) __attribute__ ((naked));
void SVC_Handler(void) {
  asm volatile (
    サービスコール番号と引数の処理
    "push {lr}\n\t"
    "bl %[a]\n\t"
    "pop {lr}\n\t"
    "bx lr\n\t"
    ::[a] "g"(SVCExec)
  );
}

これで動作すると思ったらタスクがスタートしない。
タスクがスタートする前にユーザモードに遷移するサービスコールをコールしている。
特権モードからユーザモードに遷移するには、lrレジスタの特定のビットを設定する。
あらたに追加したSVCExec関数でlrレジスタを設定していたので、SVCExec関数を抜けるときにlrレジスタが上書きされていた。
ユーザモードの遷移処理はSVC_Handlerで行う必要がある。
次のように変更して、やっと正常に動作した。想像以上に大変だったので同じことで悩むことが少なくなるように投稿しました。

void SVC_Handler(void) __attribute__ ((naked));
void SVC_Handler(void) {
  asm volatile (
    "mov r1, r1, lsl #8\n\t"
    "orr r0, r0, r1\n\t"
    "mov r1, lr\n\t"
    "ands r1,#4\n\t"
    "beq .L0000\n\t"
    "mrs r1,psp\n\t"
    "b .L0001\n\t"
    ".L0000:\n\t"
    "mrs r1,msp\n\t"
    ".L0001:\n\t"
    "ldr r3,[r1,#24]\n\t"
    "ldr r2,[r3,#-2]\n\t"
    "and r2, r2, #0xff\n\t"
    "push {r2}\n\t"   // 上書きされた。。。
    "push {lr}\n\t"
    "bl %[a]\n\t"
    "pop {lr}\n\t"
    "pop {r2}\n\t"    // 上書きを防ぐため退避したR2を復帰
    "cmp r2, #0xff\n\t"
    "bne .L0002\n\t"
    "orr lr,lr,#4\n\t"
     ".L0002:\n\t"
    "bx lr\n\t"
    ::[a] "g"(SVCExec)
  );
}

Makefileと参考文献

  • 参考文献

armでOS超入門 CQ出版
公開コピー誌 Linux上でのアセンブラ 暗黒通信団
Linux関係メモ@宇治屋電子 arm-inline asm
infocenter.arm.com ARM コンパイラツールチェーン リファレンス

  • Makefile
TARGET=microMulti
INCLUDES+=-I ../

include ../../makefile.conf

#STARTUP_DEFS=-D__STARTUP_CLEAR_BSS -D__START=main

SRCS=../main.c
#SRCS+=../stm32f10x_it.c
SRCS+=$(LIBRARY_SRC_DIR)/stm32f10x_gpio.c
SRCS+=$(LIBRARY_SRC_DIR)/stm32f10x_rcc.c
SRCS+=$(LIBRARY_SRC_DIR)/misc.c
SRCS+=$(DEVICE_SUPPORT_DIR)/system_stm32f10x.c
SRCS+=$(UTILITY_DIR)/STM32vldiscovery.c

OBJS:=$(notdir $(SRCS:.c=.o))
STARTUP_OBJ=$(notdir $(STARTUP:.s=.o))

# Need following option for LTO as LTO will treat retarget functions as
# unused without following option
CFLAGS+=-fno-builtin
CFLAGS+=$(USE_NANO)
CFLAGS+=$(USE_NOHOST)
CFLAGS+=-Wall
CFLAGS+=-DSTM32F10X_MD_VL
CFLAGS+=-DUSE_STDPERIPH_DRIVER

LDSCRIPTS=-L. -L../TrueSTUDIO -T stm32_flash.ld

LFLAGS=$(USE_NANO) $(USE_NOHOST) $(LDSCRIPTS) $(GC) $(MAP) -Wl,--cref

$(TARGET).bin : $(TARGET)
    $(OBJCOPY) $(TARGET) -I ihex -O binary $(TARGET).bin

$(TARGET): $(OBJS) $(STARTUP_OBJ)
    $(CC) -o $@ $(LFLAGS) $^

$(OBJS): $(SRCS) $(STARTUP)
    $(CC) $(CFLAGS) $(INCLUDES)  -c $(SRCS) $(STARTUP)

clean:
    rm -f *.axf *.map *.o $(TARGET) $(TARGET).bin

makefile.conf

# Selecting Core
CORTEX_M=3

# Use newlib-nano. To disable it, specify USE_NANO=
USE_NANO=--specs=nano.specs

# Use seimhosting or not
USE_SEMIHOST=--specs=rdimon.specs
USE_NOHOST=--specs=nosys.specs

CORE=CM$(CORTEX_M)
BASE=../..

# Compiler & Linker
CC=arm-none-eabi-gcc
CXX=arm-none-eabi-g++
AR=arm-none-eabi-ar
OBJCOPY=arm-none-eabi-objcopy

# Options for specific architecture
ARCH_FLAGS=-mthumb -mcpu=cortex-m$(CORTEX_M)
ARFLAG=crsv

# Startup code
#STARTUP=$(BASE)/startup/startup_ARM$(CORE).S
STARTUP=$(BASE)/Libraries/CMSIS/CM3/DeviceSupport/ST/STM32F10x/startup/gcc_ride7/startup_stm32f10x_ld_vl.s

#Library directory
LIBRARY_SRC_DIR=$(BASE)/Libraries/STM32F10x_StdPeriph_Driver/src

#device support directory
DEVICE_SUPPORT_DIR=$(BASE)/Libraries/CMSIS/CM3/DeviceSupport/ST/STM32F10x
CORE_SUPPORT_DIR=$(BASE)/Libraries/CMSIS/CM3/CoreSupport

#utility directory
UTILITY_DIR=$(BASE)/Utilities

#include directory
INCLUDES+=-I $(UTILITY_DIR)/
INCLUDES+=-I $(DEVICE_SUPPORT_DIR)/
INCLUDES+=-I $(CORE_SUPPORT_DIR)/
INCLUDES+=-I $(BASE)/Libraries/STM32F10x_StdPeriph_Driver/inc

# -Os -flto -ffunction-sections -fdata-sections to compile for code size
#CFLAGS=$(ARCH_FLAGS) $(STARTUP_DEFS) -Os -flto -ffunction-sections -fdata-sections
CFLAGS=$(ARCH_FLAGS) -Os -flto -ffunction-sections -fdata-sections
CXXFLAGS=$(CFLAGS)

# Link for code size
GC=-Wl,--gc-sections

# Create map file
MAP=-Wl,-Map=$(NAME).map
4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3