ICOM IC-T90のバッテリ換装をしてみた話
おことわり
- 本記事で紹介する内容はわたし自身が自身の責任のもと実施した例を紹介するにすぎません.
- 本記事を参考して生じた事故,火災やその他の損害等が生じた場合でも,いかなる理由に関わらず一切の責任を負いかねます.
- 一般的な二次電池,得に高エネルギー密度であるリチウムイオンバッテリーは取り扱いを誤るとたやすく爆発・火災等を生じる危険性があります.このことに十分配慮する必要があります.
- この内容は製造元であるアイコム株式会社とは一切関係がなく,また推奨されることではありません.保証等が一切受けられなくなることをご理解ください.
- もしも参考にされる場合でも,上記のことを十分ご理解頂いた上で事故なく実施いただけることを願います.
コトの発端
しばらく前に手に入れていたIC-T90だが,付属のバッテリーが使い物にならなくなった.具体的にはチャージャーで充電しても5W送信した瞬間に電源が落ちる.また充電中にバッテリーが異常と思われるほどひどく発熱していたことがあった.このままでは爆発・火災になる恐れがあると判断してしばらく利用をやめていたが,どうにか使い続けられるようにできないかと色々思考錯誤してみていました.具体的には11Vの外部電源端子からの給電のみとして利用し,バッテリは外して運用することや,バッテリの交換を実施することでした.新規に純正バッテリを購入することも検討しましたが,バッテリの値段は安くなく,それなら11Vの外部電源から運用したほうがコスパはよさそうだ,などと考えていました.
しかしどうにも11Vの外部電源だとモビリティがよくありません.せっかくのハンディ機なのにいちいち外部電源のことを考えて用意しておく必要が出てきます.これでは使いにくいと考え,思いついたのがバッテリパックを分解・改造したバッテリの換装でした.ただ,リチウムイオンバッテリパックの改造は多くのリスクを伴うものであり幾度か考え直しましたが,保護回路付きのセルに入れ替えることである程度の安全性を担保しつつ,換装が行えないかと考えました.
早速換装
ICOMのバッテリパックです.BP-217という型番がついていて,7.4Vの1500mAhのものとなっています.
当初これを記事にする予定はなかったこともあり,分解時の写真が残っておらずに申し訳ないのですが,バッテリパックは側面の隙間にマイナスドライバ等を差し込んでこじ開けることができました.中にバッテリがいるということに注意をして,金属ではなくプラスチックのドライバを利用するなどするほうが安全でしょう.中には18500のリチウムイオンバッテリの生セルが2本,ニッケルのタブでスポット溶接されて内蔵されていました.セル外装はピンク色. タブ,電池の端子等をショートさせないよう十分注意を払いタブはニッパで切断してセルを取り出しました.セルさえ取り出し,絶縁してしまえば危険はひとまずありません.
分解時の写真が無いため,いきなり換装後の写真を紹介します.
このように,既存のバッテリパックの外装のみを再利用し,その中に新たに保護回路付きのセルを内蔵するような設計にしました.詳細を追って紹介します.
換装するバッテリとして選択したのはKEEPPOWERに18350, 1200mAhの保護回路付きのセルです. 値段も2セルで2000円未満と十分手頃です. www.amazon.co.jp
これを選ぶまでの過程はいくつかあります.要素としては下記を考えました.
1. もとのバッテリーパックの外装に内蔵できる大きさであること(絶対)
2. 内蔵した際に得られる公称電圧が7.4Vとなること(つまりバッテリパックの中で2Sのセルとして構成できること)
3. 保護回路がついていること(絶対)
4. できる限り容量が大きいものであること
5. さらなる交換・換装が可能であること
3の保護回路がついていることは絶対条件として考えていました.火災や爆発は避けたいからです.
1, 2の内容について,次のような結論を得ました.
もとのバッテリパックに入っているセルは生セルの18500セルとなっています.BP-217そのセルに最適化されたバッテリパックのサイズになっており,横幅には薄いニッケルのタブだけが収められる程度のサイズ余裕しかありません.しかし保護回路が搭載されたセルは一般の生セルよりも数mm長くなってしまいます.このことから同じサイズである18500の保護回路付きのセルはバッテリパックの外装に収めることはサイズ的余裕がないためできません.18500よりも小さいサイズとしては18490, 18350等があります.一般に電池の容量が増加すると電池自体のサイズも増加することから,4の条件を考えると18490サイズで保護回路付きのものがあり,全長がバッテリパックの外装に収められればこれを選びたいと考えるところでしたが,どうやら調べてみると18490のバッテリというのはあまり流通していないようで入手性がよくないようです.このことから今回は18350のサイズのセルを選ぶことにしました.また,このサイズにすることで5の条件である交換・換装用意になるようなしくみをバッテリパック内部に取り入れることが可能になりそうだという考えが浮かびます.
この時点で,上記のKEEPPOWERの18350保護回路付きセルはこれらの条件をすべて満たしており,さらに評判も悪くないメーカーであること,数多く利用され,実績のあるセイコーインスツルメンツの保護回路ICを利用していると謳われていることから上記を選択しています.
今回はこの5の条件である換装容易性を,通常の乾電池ボックス等で利用されているようなバネ性のあるコンタクトが利用できそうだと考えました.これは秋月電子で販売されているものを利用しました.ターミナルクリップと呼ばれているんですね. akizukidenshi.com
これら18350セル2本,上記のターミナルクリップ,その他配線類をもとのバッテリーパックの外装に収められれば求めるものができます.ここで懸念するべき点がいくつかあります.
1. リチウムイオンバッテリの特性がもともと入っているものと異なる可能性があること
2. 容量が異なること
少なくともこの点については気をつけなければなりません.いくら保護回路が入っているといえど,リチウムイオンバッテリで比較的特にリスクが有る作業が充電です.
充電はこの1, 2のことをよく考慮して実施する必要があります.リチウムイオンバッテリは一般に最大充電電流は1Cとされています.このセルは1200mAh, もとのセルは1500mAhです.この時点でIC-T90の急速充電装置およびIC-T90本体からの補助充電時の充電電流が高すぎる恐れがあるということは容易に想像できます.このことを気にかけておく必要があります.
また放電についても,もとのバッテリの放電特性が不明であることから,気にかける必要があります.しかしリチウムイオンバッテリの放電特性として低内部抵抗で比較的大電流を扱うことができることからそこまで大きな問題になることは少ないでしょう.KEEPPOWERのこのバッテリはの出力電流は最大4.1C出力の仕様であり,最大放電電流は約5A程度になります.あらかじめIC-T90のバッテリ運用時の必要電流を測定しましたが,5Aは大きく下回っていることを確認していましたので,この点についてはクリアです.
このことを忘れないようにしておき,作成後に検証することにします.
さて,長くなりましたが作成物の紹介をしていきます.購入したバッテリはこれ.ちゃんとケースもついてきて2セルで1500円くらいです.
バッテリとコンタクトするターミナルクリップをうまく固定するため,3Dプリンタで保持具を作ります.モデルはこんな感じ.
この保持具にコンタクトを接着剤で固定し,配線類のはんだ付けを行っています.配線類はバッテリから2-3A程度の電流をとっても発熱が少なくなるよう余裕を持った太さのものを使うと良いでしょう.
見ていただくとわかると思いますが,バッテリパックには制御基板が入っています.これはもともと生セルと一緒に入っていた制御基板をそのまま利用しています.制御基板からはそれぞれ充電用端子,バッテリの各セルのプラスマイナスに対して配線が伸びているので,もとの生セルと同様の配線になるように配線してあげます.要するに生セルだろうが換装後の保護回路付きセルだろうが配線は変えずに生セルだと思って配線してあげればよいです.これで制御基板側でも充電・放電電流の監視も行ってくれることでしょう.セル側にも最後の頼みの綱として保護回路があり,2重で保護されている形になります.
両側面にはエポキシ接着剤をいれて固定してしまっています.
斜めからみるとこう.バッテリを入れる前に抵抗計でバッテリのセルを入れても問題ないか(セル両端で抵抗値測定をした場合に異常に低い値を示さないか)を確認しておくと良いでしょう.
これでバッテリパック自体は構成できました.この時点で電圧を確認します.低くても5V程度から高くても8.4V程度である必要があるでしょう.リチウムイオンバッテリの終止電圧は低くてもセルあたり2.5V程度,満充電電圧はセルあたり4.2V程度ですから,これに由来します.この範囲を大きく外れていた場合はセルや回路に異常があると判断して調査を行ったほうが良いかもしれません.
さて,電圧の正常性が確認できたら充電電流を測定してみます.今回は急速充電クレードルBC-139を利用して測定を行いました.電流計,電圧計をかましてセル状態を監視しながら充電をしてみます.最大充電電流は1Cですから,1.2Aを超えていた場合は即座に充電を中止する心構えをしておきます.
測定中の画像が下記です.クレードルの端子にはワニ口クリップをはさみ充電・測定系につなぎます.クレードルの下にはスイッチがついており,このスイッチが押されていると充電を開始しますから,これを押して計測します.はじめは手で押して様子を見て,問題なさそうならマスキングテープで押しっぱなしにして充電完了まで監視します.
測定の結果,終止電圧付近から充電を始めた場合でも,充電電流は最大0.8A程度で,1Aをこえることはありませんでした.このことから約0.66C程度で充電をしている事になりますが,これは1Cを大きく下回っていること,一般に充電電流は0.5C-1Cの間で行うことが多いことから,0.66C充電は問題ないものと判断しました.実際危険はあまりないでしょう.
充電は無事完了し,充電クレードル側でも充電完了を示す緑色のランプが点灯し充電電流が0Aになり充電が終了しました. 充電が無事成功したといえるでしょう.
換装後充電したバッテリパックで送信・受信ともにテストを行いましたが5W送信も問題なく行え,電池本体や周辺回路からの発熱もほぼありませんでした.それより5W送信すると本体の発熱が大きいです.
ちなみに充電中であっても電池本体・回路からの発熱はほぼありません.それより充電クレードル本体の発熱がひどいです.
というわけで無事バッテリの換装ができ,ハンディ機として継続して利用することができるようになりました.1500mhAから1200mhAに容量が減っていることから,多少運用時間は短くなることは想定されますが,その場合はセル自体が交換できるようになっていますので,同様の18350保護回路付きセルをケースに入れて持ち運び,交換してあげるような運用もできます.これで使いやすくなりました.めでたし.
まとめ
STM32 HAL タイマ割り込みの基礎の基礎
おことわり・前提
- STM32CubeIDEを使った話をします.
- あくまで自分用のメモという目的が主.
- 逐次updateしたり追記したりがあるかもしれません.
本題
- タイマ割り込みを使えるようにする.
- CubeMXで利用したいタイマを有効にする.
- プリスケーラ,カウンタを適切に計算してセット.
- 基本的にtimer clock sourceはinternal clockならAPB系がつかわれるが,どの系でくるかはdatasheet要確認.
- auto preloadをenableにしておくと楽.
- overしたときのcnt resetを自動でやってくれる.
- タイマ割り込みをenableにする.
- 忘れるとハマる.
- どこかで
HAL_TIM_Base_Start_IT(&htimx);
をcallしてtimer interruptをenableにする.
コードを見ていく
./Drivers/STM32F0xx_HAL_Driver/Src/stm32f0xx_hal_tim.c
HAL_StatusTypeDef HAL_TIM_Base_Start_IT(TIM_HandleTypeDef *htim) { uint32_t tmpsmcr; /* Check the parameters */ assert_param(IS_TIM_INSTANCE(htim->Instance)); /* Enable the TIM Update interrupt */ __HAL_TIM_ENABLE_IT(htim, TIM_IT_UPDATE); /* Enable the Peripheral, except in trigger mode where enable is automatically done with trigger */ tmpsmcr = htim->Instance->SMCR & TIM_SMCR_SMS; if (!IS_TIM_SLAVEMODE_TRIGGER_ENABLED(tmpsmcr)) { __HAL_TIM_ENABLE(htim); } /* Return function status */ return HAL_OK; }
- 基本的に
main.cpp
のstatic void MX_TIMx_Init(void)
あたりの最後で呼び出せばいいと思う.- もちろんmain関数の頭らへん(
MX_TIM3_Init()
が呼ばれたあと)で呼んでももちろん動くはず. - ex.
- もちろんmain関数の頭らへん(
/** * @brief TIM3 Initialization Function * @param None * @retval None */ static void MX_TIM3_Init(void) { /* USER CODE BEGIN TIM3_Init 0 */ /* USER CODE END TIM3_Init 0 */ TIM_ClockConfigTypeDef sClockSourceConfig = {0}; TIM_MasterConfigTypeDef sMasterConfig = {0}; /* USER CODE BEGIN TIM3_Init 1 */ /* USER CODE END TIM3_Init 1 */ htim3.Instance = TIM3; htim3.Init.Prescaler = 39999; htim3.Init.CounterMode = TIM_COUNTERMODE_UP; htim3.Init.Period = 199; htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; if (HAL_TIM_Base_Init(&htim3) != HAL_OK) { Error_Handler(); } sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL; if (HAL_TIM_ConfigClockSource(&htim3, &sClockSourceConfig) != HAL_OK) { Error_Handler(); } sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; if (HAL_TIMEx_MasterConfigSynchronization(&htim3, &sMasterConfig) != HAL_OK) { Error_Handler(); } /* USER CODE BEGIN TIM3_Init 2 */ HAL_TIM_Base_Start_IT(&htim3); /* USER CODE END TIM3_Init 2 */ }
main.cpp
にcallbackを書く- 複数あるときでも引数で渡される
&htimx
を見て比較して分岐する. ./Core/Src/main.cpp
- 複数あるときでも引数で渡される
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim){ if(htim == &htim3) HAL_GPIO_TogglePin(GPIOF, GPIO_PIN_0); if(htim == &htim14) printf("htim14 callback!\r\n"); }
- 大まかな流れと実装.
./Core/Src/stm32f0xx_it.c
- それぞれの割り込みハンドラがここにいる.
- タイマの割り込みハンドラは共通の
HAL_TIM_IRQHandler(&htimx)
をcallする.
/** * @brief This function handles TIM3 global interrupt. */ void TIM3_IRQHandler(void) { /* USER CODE BEGIN TIM3_IRQn 0 */ /* USER CODE END TIM3_IRQn 0 */ HAL_TIM_IRQHandler(&htim3); /* USER CODE BEGIN TIM3_IRQn 1 */ /* USER CODE END TIM3_IRQn 1 */ }
./Drivers/STM32F0xx_HAL_Driver/Src/stm32f0xx_hal_tim.c
HAL_TIM_IRQHandler(TIM_HandleTypeDef *htim)
は共通のタイマハンドラ.- ここでタイマの各種イベントに応じてcallback先を選んでいる.
- タイマの時間切れ(elasped)の場合update eventとなり,タイマ共通の
HAL_TIM_PeriodElapsedCallback(htim)
がcallされるが,htimはそれぞれのIRQHandlerで引数として渡したタイマのオブジェクト(address)が入っているのでhtimを見ることでどのタイマによって発生したイベントかを判断できる.
/** * @} */ /** @defgroup TIM_Exported_Functions_Group7 TIM IRQ handler management * @brief TIM IRQ handler management * @verbatim ============================================================================== ##### IRQ handler management ##### ============================================================================== [..] This section provides Timer IRQ handler function. @endverbatim * @{ */ /** * @brief This function handles TIM interrupts requests. * @param htim TIM handle * @retval None */ void HAL_TIM_IRQHandler(TIM_HandleTypeDef *htim) { ...(snip)... /* TIM Update event */ if (__HAL_TIM_GET_FLAG(htim, TIM_FLAG_UPDATE) != RESET) { if (__HAL_TIM_GET_IT_SOURCE(htim, TIM_IT_UPDATE) != RESET) { __HAL_TIM_CLEAR_IT(htim, TIM_IT_UPDATE); #if (USE_HAL_TIM_REGISTER_CALLBACKS == 1) htim->PeriodElapsedCallback(htim); #else HAL_TIM_PeriodElapsedCallback(htim); #endif /* USE_HAL_TIM_REGISTER_CALLBACKS */ } } ...(snip)... }
STM32でSWCLK, SWDIOをGPIO Outputにして書き込めずにハマる
STM32 HAL GPIOの基礎の基礎
おことわり・前提
- STM32CubeIDEを使った話をします.
- あくまで自分用のメモという目的が主.
- 逐次updateしたり追記したりがあるかもしれません.
本題
- tips: GPIOのlabelはただのエイリアス.
./Core/Inc/main.h
#define LCD_RS_Pin GPIO_PIN_6 #define LCD_RS_GPIO_Port GPIOA
- エイリアスの先はちゃんとdriverのheaderで定義されている.なのでlabelをつけたからといって
GPIOA
やGPIO_PIN_X
などで指定できなくなるというわけではない../Drivers/STM32F0xx_HAL_Driver/Inc/stm32f0xx_hal_gpio.h
...(snip)... /* Exported constants --------------------------------------------------------*/ /** @defgroup GPIO_Exported_Constants GPIO Exported Constants * @{ */ /** @defgroup GPIO_pins GPIO pins * @{ */ #define GPIO_PIN_0 ((uint16_t)0x0001U) /* Pin 0 selected */ #define GPIO_PIN_1 ((uint16_t)0x0002U) /* Pin 1 selected */ #define GPIO_PIN_2 ((uint16_t)0x0004U) /* Pin 2 selected */ #define GPIO_PIN_3 ((uint16_t)0x0008U) /* Pin 3 selected */ #define GPIO_PIN_4 ((uint16_t)0x0010U) /* Pin 4 selected */ #define GPIO_PIN_5 ((uint16_t)0x0020U) /* Pin 5 selected */ #define GPIO_PIN_6 ((uint16_t)0x0040U) /* Pin 6 selected */ #define GPIO_PIN_7 ((uint16_t)0x0080U) /* Pin 7 selected */ #define GPIO_PIN_8 ((uint16_t)0x0100U) /* Pin 8 selected */ #define GPIO_PIN_9 ((uint16_t)0x0200U) /* Pin 9 selected */ #define GPIO_PIN_10 ((uint16_t)0x0400U) /* Pin 10 selected */ #define GPIO_PIN_11 ((uint16_t)0x0800U) /* Pin 11 selected */ #define GPIO_PIN_12 ((uint16_t)0x1000U) /* Pin 12 selected */ #define GPIO_PIN_13 ((uint16_t)0x2000U) /* Pin 13 selected */ #define GPIO_PIN_14 ((uint16_t)0x4000U) /* Pin 14 selected */ #define GPIO_PIN_15 ((uint16_t)0x8000U) /* Pin 15 selected */ #define GPIO_PIN_All ((uint16_t)0xFFFFU) /* All pins selected */ ...(snip)...
STM32 HAL PWMの基礎の基礎
おことわり・前提
- STM32CubeIDEを使った話をします.
- あくまで自分用のメモという目的が主.
- 逐次updateしたり追記したりがあるかもしれません.
本題
- PWMを出すには
- CubeMXで利用したいタイマを選択.channelの設定でどのピンをPWM出力として使うかを指定.
- 普通のタイマの設定と同様にプリスケーラ,カウンタを設定.
- cubeMXでgenerateされるコードはconfigまでは入るがそのままではPWMがstartしない.
HAL_TIM_PWM_Start();
をcallする必要がある.MX_TIMx_Init()
の最後で呼んでおくと良さそう.- 別にmainで利用時にstartするのでも問題ない.
コードを見ていく
/** * @brief TIM1 Initialization Function * @param None * @retval None */ static void MX_TIM1_Init(void) { /* USER CODE BEGIN TIM1_Init 0 */ /* USER CODE END TIM1_Init 0 */ TIM_ClockConfigTypeDef sClockSourceConfig = {0}; TIM_MasterConfigTypeDef sMasterConfig = {0}; TIM_OC_InitTypeDef sConfigOC = {0}; TIM_BreakDeadTimeConfigTypeDef sBreakDeadTimeConfig = {0}; /* USER CODE BEGIN TIM1_Init 1 */ /* USER CODE END TIM1_Init 1 */ htim1.Instance = TIM1; htim1.Init.Prescaler = 9; htim1.Init.CounterMode = TIM_COUNTERMODE_UP; htim1.Init.Period = 799; htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim1.Init.RepetitionCounter = 0; htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE; if (HAL_TIM_Base_Init(&htim1) != HAL_OK) { Error_Handler(); } sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL; if (HAL_TIM_ConfigClockSource(&htim1, &sClockSourceConfig) != HAL_OK) { Error_Handler(); } if (HAL_TIM_PWM_Init(&htim1) != HAL_OK) { Error_Handler(); } sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; if (HAL_TIMEx_MasterConfigSynchronization(&htim1, &sMasterConfig) != HAL_OK) { Error_Handler(); } sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = 100; sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.OCNPolarity = TIM_OCNPOLARITY_HIGH; sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; sConfigOC.OCIdleState = TIM_OCIDLESTATE_RESET; sConfigOC.OCNIdleState = TIM_OCNIDLESTATE_RESET; if (HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_1) != HAL_OK) { Error_Handler(); } sConfigOC.Pulse = (uint32_t)htim1.Init.Period*0.4; if (HAL_TIM_PWM_ConfigChannel(&htim1, &sConfigOC, TIM_CHANNEL_4) != HAL_OK) { Error_Handler(); } sBreakDeadTimeConfig.OffStateRunMode = TIM_OSSR_DISABLE; sBreakDeadTimeConfig.OffStateIDLEMode = TIM_OSSI_DISABLE; sBreakDeadTimeConfig.LockLevel = TIM_LOCKLEVEL_OFF; sBreakDeadTimeConfig.DeadTime = 0; sBreakDeadTimeConfig.BreakState = TIM_BREAK_DISABLE; sBreakDeadTimeConfig.BreakPolarity = TIM_BREAKPOLARITY_HIGH; sBreakDeadTimeConfig.AutomaticOutput = TIM_AUTOMATICOUTPUT_DISABLE; if (HAL_TIMEx_ConfigBreakDeadTime(&htim1, &sBreakDeadTimeConfig) != HAL_OK) { Error_Handler(); } /* USER CODE BEGIN TIM1_Init 2 */ HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_4); /* USER CODE END TIM1_Init 2 */ HAL_TIM_MspPostInit(&htim1); }
- 例えばこんな関数を作っておくとdutyをtimer/channelごとに更新できる.
void setPWM(TIM_HandleTypeDef *htim, uint32_t Channel, float duty) { TIM_OC_InitTypeDef sConfigOC = {0}; sConfigOC.OCMode = TIM_OCMODE_PWM1; sConfigOC.Pulse = (uint32_t)htim1.Init.Period*duty; sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; sConfigOC.OCNPolarity = TIM_OCNPOLARITY_HIGH; sConfigOC.OCFastMode = TIM_OCFAST_DISABLE; sConfigOC.OCIdleState = TIM_OCIDLESTATE_RESET; sConfigOC.OCNIdleState = TIM_OCNIDLESTATE_RESET; if (HAL_TIM_PWM_ConfigChannel(htim, &sConfigOC, Channel) != HAL_OK) { Error_Handler(); } HAL_TIM_PWM_Start(htim, Channel); }
- 相補PWMするときは
HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_1);
をcallしなければ動かない
STM32 HAL 外部割り込みの基礎の基礎
おことわり・前提
- STM32CubeIDEを使った話をします.
- あくまで自分用のメモという目的が主.
- 逐次updateしたり追記したりがあるかもしれません.
本題
- 外部割り込みをするには
- GPIOをEXTI Interrupt modeにする.
- NVICで該当のEXTI Interruptをenableにする.
- これを忘れるとEXTIされないので注意.ハマりがち.
- 外部割り込みに限らずなんだけれども,割り込みの優先順位は適切につける.
- 例えば外部割り込みの中で
HAL_Delay()
を呼んだりしたいときはTime base: System tick timer
より外部割り込みの優先順位を下げないと処理が戻ってこなかったりする.
- 例えば外部割り込みの中で
実際のコードをみて割り込みの流れをみる
./Core/Src/stm32f0xx_it.c
/******************************************************************************/ /* STM32F0xx Peripheral Interrupt Handlers */ /* Add here the Interrupt Handlers for the used peripherals. */ /* For the available peripheral interrupt handler names, */ /* please refer to the startup file (startup_stm32f0xx.s). */ /******************************************************************************/ /** * @brief This function handles EXTI line 0 and 1 interrupts. */ void EXTI0_1_IRQHandler(void) { /* USER CODE BEGIN EXTI0_1_IRQn 0 */ /* USER CODE END EXTI0_1_IRQn 0 */ HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_1); /* USER CODE BEGIN EXTI0_1_IRQn 1 */ /* USER CODE END EXTI0_1_IRQn 1 */ }
- 複数のEXTIピンを定義した場合でも同じ
HAL_GPIO_EXTI_IRQHandler()
がcallされる.
/******************************************************************************/ /* STM32F0xx Peripheral Interrupt Handlers */ /* Add here the Interrupt Handlers for the used peripherals. */ /* For the available peripheral interrupt handler names, */ /* please refer to the startup file (startup_stm32f0xx.s). */ /******************************************************************************/ /** * @brief This function handles EXTI line 0 and 1 interrupts. */ void EXTI0_1_IRQHandler(void) { /* USER CODE BEGIN EXTI0_1_IRQn 0 */ /* USER CODE END EXTI0_1_IRQn 0 */ HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_1); /* USER CODE BEGIN EXTI0_1_IRQn 1 */ /* USER CODE END EXTI0_1_IRQn 1 */ } /** * @brief This function handles EXTI line 2 and 3 interrupts. */ void EXTI2_3_IRQHandler(void) { /* USER CODE BEGIN EXTI2_3_IRQn 0 */ /* USER CODE END EXTI2_3_IRQn 0 */ HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_3); /* USER CODE BEGIN EXTI2_3_IRQn 1 */ /* USER CODE END EXTI2_3_IRQn 1 */ }
HAL_GPIO_EXTI_IRQHandler()
がcallされると割り込みビットの処理が実行され,HAL_GPIO_EXTI_Callback()
がcallされる.- これがユーザが実際に定義することを想定しているcallback functionである.
stm32f0xx_hal_gpio.c
ですでに__weak void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
という形で定義されているが,__weak
指定子がついているので,ユーザがvoid HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
を別で定義した場合それが優先される.
./Drivers/STM32F0xx_HAL_Driver/Src/stm32f0xx_hal_gpio.c
/** * @brief Handle EXTI interrupt request. * @param GPIO_Pin Specifies the port pin connected to corresponding EXTI line. * @retval None */ void HAL_GPIO_EXTI_IRQHandler(uint16_t GPIO_Pin) { /* EXTI line interrupt detected */ if(__HAL_GPIO_EXTI_GET_IT(GPIO_Pin) != 0x00u) { __HAL_GPIO_EXTI_CLEAR_IT(GPIO_Pin); HAL_GPIO_EXTI_Callback(GPIO_Pin); } } /** * @brief EXTI line detection callback. * @param GPIO_Pin Specifies the port pin connected to corresponding EXTI line. * @retval None */ __weak void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { /* Prevent unused argument(s) compilation warning */ UNUSED(GPIO_Pin); /* NOTE: This function should not be modified, when the callback is needed, the HAL_GPIO_EXTI_Callback could be implemented in the user file */ }
- ユーザは実際に
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
を定義して利用することができる.- 引数として
GPIO_PIN
が渡されるので,これを見てどのピンのEXTI割り込みかを判断する.
- 引数として
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin){ HAL_Delay(1); printf("EXTI callback called!\r\n"); if (GPIO_Pin == GPIO_PIN_3) printf("gpio3 pushed!\r\n"); if (GPIO_Pin == GPIO_PIN_1) printf("gpio1 released!\r\n"); }
- STM32 external interrupt and tick timer - Programmer Soughte
- EXTI内部で
HAL_Delay()
を叩くとすべての処理が停止することがあった. - これは割り込み優先度がすべて
0
となっていたため. HAL_Delay()
はsystick
を利用するためこの割り込みの優先度をあげ,EXTI
割り込みの優先度を下げる必要がある.- これにより無限ループによる処理停止が引き起こされない.
- EXTI内部で
STM32 HAL ADCの基礎の基礎
おことわり・前提
- STM32CubeIDEを使った話をします.
- あくまで自分用のメモという目的が主.
- 逐次updateしたり追記したりがあるかもしれません.
本題
DMAで連続変換するときの鍵
- clockはsystem clock devidedなものを入れると楽.Async clockは別途記述が必要.
- continuous conversion enableにしてDMA continuous request enableにしておくとDMA割り込みが入りまくることに注意.ADCが終わるたびにDMA走る.
- これを防ぐにはcontinuous conversion disableとして普通にタイマ割り込みでADCを回すのが良さそう.DMA continuous requestはenableのまま.
- DMAはcircularを選ぶ
- DMAのmemory はincrementにしておく
- DMA continuous request enable
- 上記をやっておかないと連続変換されなかったり,DMAに連続的に変換結果が入らずにハマる.
- もちろん理解して変更すればいいが,基本上記でいいかなぁという気持ち.
code examples
constexpr adcChannels = 3; //number of using ADC channels static uint16_t adcData[adcChannels]; if (HAL_ADC_Start_DMA(&hadc,(uint32_t *)adcData, adcChannels) != HAL_OK) Error_Handler();
これで逐次clockに応じてADCが動作しDMAで adcData[]
に変換結果が転送される.
変換完了後にcallbackしたい場合は下記等が便利.変換完了時にcallbackされる.
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *AdcHandle){ //do something }
- キャリブレーションが必要な場合は下記を呼び出すことでADCのキャリブレーションが可能らしい. しかしこれは通常起動時に実施済みであるため明示的には不要の場合が多いが,長時間駆動する機器の場合定期的にキャリブレーションを実施することにより精度の高い測定が可能となるよう.
if(HAL_ADCEx_Calibration_Start(&hadc) != HAL_OK)
Error_Handler();
タイマ割り込みにする場合
- CubeMXでADCのconfigを下記のようにする.ADCのチャネル,DMA転送など基本的なconfigはできている前提で
ADC_Regular_ConversionMode
->External Trigger Conversion Source
をTimer1 Trigger Out event
に.
- さらにCubeMXでTIM Timer1のconfigをする.プリスケーラやカウンタ値等のタイマ割り込みを発生させるconfigは書けている前提で
Trigger Output(TRGO) Parameters
->Trigger Event Selection
をUpdate Event
に. これでタイマ割り込みでADCがトリガされるはず.
- CubeMXでADCのconfigを下記のようにする.ADCのチャネル,DMA転送など基本的なconfigはできている前提で
ADCのキャネルとデータの入り方
ch1, ch2, ch3, ch4, ch5, ch6, ch7
を有効にしていたとして
constexpr adcChannels = 8; //number of using ADC channels static uint16_t adcData[adcChannels]; if (HAL_ADC_Start_DMA(&hadc,(uint32_t *)adcData, adcChannels) != HAL_OK) Error_Handler();
このようなコードを書いた場合,変換結果は
ch1: adcData[0]; ch2: adcData[1]; ch3: adcData[2]; ch4: adcData[3]; ch5: adcData[4]; ch6: adcData[5]; ch7: adcData[6]; ch8: adcData[7];
に対応して格納される.
ch1
, ch3
, ch5
, ch7
を有効(ch2
, ch4
, ch6
は無効)にしていたとして
constexpr adcChannels = 4; //number of using ADC channels static uint16_t adcData[adcChannels]; if (HAL_ADC_Start_DMA(&hadc,(uint32_t *)adcData, adcChannels) != HAL_OK) Error_Handler();
このようなコードを書くと
ch1: adcData[0]; ch3: adcData[1]; ch5: adcData[2]; ch7: adcData[3];
仮にここで
constexpr adcChannels = 3; //number of using ADC channels
としてしまった場合,書くchの変換結果が同一の添字(配列index)で取得できなくなる.
具体的にはリングバッファのようにずれてしまう.
したがってHAL_ADC_Start_DMA()
に渡すbufはchに対し十分余裕を持ちlengthは有効化しているadcチャネルと同一値とすることが大変重要である.
有効化しているadcチャネル数よりも少なくても多くてもずれが生じる.
この状態で気づかずに変換結果を取得すると逐次値が変動してハマる.ノイズのよう見見えてしまうことがあるかもしれない.
念の為adcの各chが同一の添字で同じものが取り続けられているかをserial(uart)等を利用し事前に確認しておくことが望ましい.
- ちなみにプロトタイプはこんな感じになっている:
HAL_StatusTypeDef HAL_ADC_Start_DMA(ADC_HandleTypeDef* hadc, uint32_t* pData, uint32_t Length)