STM32マイコンでLIN通信を行った際のまとめ
| 開発環境 | STM32CubeMX(Version 5.3.0)(無料)Atollic TrueSTUDIO for STM32(Version: 9.3.0)(無料) |
| デバッガ | J-Link Plus |
| ボード | 自社製品(CanLine-1)用ボード |
| マイコン | STM32F205RCT7 |
| LINトランシーバ | SN65HVDA100[TEXAS INSTRUMENTS] |
![]()
LIN初期設定
CubeMXでLIN初期設定を行います。
使用するピンの設定をします。
- PA0→UART4_TX
- PA1→UART4_RX
- PA7→GPIO_OUTPUT(LIN_EN)
PA7はLINトランシーバのスタンバイモードを制御(High=通常モード、Low=スタンバイモード)
PA7の初期値をHighに(LINトランシーバを通常モードに)
次にUART4の「Mode」をLINにする。
LINの通信速度は一般的には2400、4800、9600、19200Bits/sなので、今回は「Baud Rate」を19200Bits/sに設定。
その他はデフォルトのまま。
ちなみに「Break Detect Length」は10ビットまたは11ビットと選択できますが、これはブレーク信号を何ビットで検知するかの設定で、LINの規格では13ビット以上なので、どちらでも検出可能だと思いますが、今回はデフォルトの10ビットのままで。(ブレーク信号を検知した際は割込みを発生させることが可能である。)
「Project Manager」の「Advanced Settings」のUARTを「HAL」→「LL」に変更。
(HALライブラリよりLLライブラリの方が実行速度が速いので)
LIN送信
LIN送信はTXエンプティー割込みを使用しました。(送信レジスタが空になると発生)
CubeMXで「UART4 global interrupt」をチェック
CubeMXで「GENERATE CODE」を押すと割込みを管理するファイル「stm32f2xx_it.c」内にUART4_IRQHandler(void)という関数が生成されます。
※「stm32f2xx_it.c」はmain.cと同じフォルダ内にある。
その中にTXエンプティー割込みが発生した際の処理を記述します。
下記サンプルコードのようにTxBuf[11]、TxLen、TxIdxという3個のグローバル変数を用意します。
TxBufの配列要素数が11なのはLINのフレームは下記の構成になっており、Breakフィールド以降のSynchフィールドからチェックサムのデータを格納するためです。
- Breakフィールド:13ビット以上のドミナント
- Synchフィールド(0x55):1バイト
- Protected ID(PID)フィールド:1バイト
- データフィールド:最大8バイト
- チェックサム:1バイト
TxLenは送信しようとするデータ長でTxIdxは送信済みデータ長です。
送信する際は変数にデータセット後、TXエンプティー割り込み有効、受信動作は無効にします。
受信動作を無効にする理由は、LINは1線式シリアル通信(送信ラインと受信ラインが共通)であるので、自分が送信したデータも受信データと認識するためです。
全てのデータを送信した後、再度受信動作を有効にします。
// main.c
uint8_t TxBuf[11]; // 送信バッファ
int TxLen; // 送信するバイト数
int TxIdx; // 送信済みバイト数
//-------------------------------------------------------------------
// LIN1.x 送信
//-------------------------------------------------------------------
void Lin1xSend()
{
TxBuf[ 0] = 0x55; // Synchフィールド
TxBuf[ 1] = PidTable[0x3C]; // PIDフィールド
TxBuf[ 2] = 1; // データフィールド
TxBuf[ 3] = 2;
TxBuf[ 4] = 3;
TxBuf[ 5] = 4;
TxBuf[ 6] = 5;
TxBuf[ 7] = 6;
TxBuf[ 8] = 7;
TxBuf[ 9] = 8;
TxBuf[10] = CalcCheckSum(8, &TxBuf[2]); // チェックサム
TxIdx = 0;
TxLen = 11;
LL_USART_DisableDirectionRx(UART4); // 受信無効
LL_USART_EnableIT_TXE(UART4); // TX Empty割り込み有効
}
PIDフィールドは6ビットのフレームIDと2ビットのパリティから構成されている。
上記計算を送信時に毎回行うのは非効率なので、マイコン起動時に予めフレームID 0~0x3FのPIDテーブルを生成する関数を自作しました。
送信時はPIDテーブルから計算結果を参照するだけです。
uint8_t PidTable[0x3F + 1]; // PIDテーブル
//-------------------------------------------------------------------
// LIN PIDテーブル生成
//-------------------------------------------------------------------
void CreatePidTable(void)
{
for(uint8_t i = 0; i <= 0x3F; i++){
uint8_t pid = i;
uint8_t P0;
uint8_t P1;
uint8_t ID0 = ((pid & 0x01) != 0)? 1 : 0;
uint8_t ID1 = ((pid & 0x02) != 0)? 1 : 0;
uint8_t ID2 = ((pid & 0x04) != 0)? 1 : 0;
uint8_t ID3 = ((pid & 0x08) != 0)? 1 : 0;
uint8_t ID4 = ((pid & 0x10) != 0)? 1 : 0;
uint8_t ID5 = ((pid & 0x20) != 0)? 1 : 0;
P0 = ID0 ^ ID1 ^ ID2 ^ ID4;
if(P0 != 0){
pid = pid | 0x40;
}
P1 = (ID1 ^ ID3 ^ ID4 ^ ID5) ^ 1;
if(P1 != 0){
pid = pid | 0x80;
}
PidTable[i] = pid;
}
}
チェックサムの計算も自作しました。標準チェックサムは主にLIN1.xで使用され、拡張チェックサムはLIN2.xで使用されます。
//-------------------------------------------------------------------
// 標準チェックサム計算
// 各データ値の総和を反転した値が格納される。
// ただし、総和の結果が桁あふれとなった場合、桁上がり値を演算結果に加算する。
// →演算対象は、すべてのデータバイト
// →LIN1.xのすべてのフレームIDに使用
// →LIN2.xでは、診断フレーム(フレームID 60(0x3C)~61(0x3D))のみ使用
//-------------------------------------------------------------------
uint8_t CalcCheckSum(uint8_t dlc, uint8_t *data)
{
uint32_t Sum = 0;
for(int i = 0; i < dlc; i++){
Sum += (uint32_t)data[i];
if(0x000000FF < Sum){
Sum = (Sum & 0x000000FF) + 1;
}
}
return (uint8_t)(~Sum & 0x000000FF);
}
//-------------------------------------------------------------------
// 拡張チェックサム計算
// 各データ値の総和を反転した値が格納される。
// ただし、総和の結果が桁あふれとなった場合、桁上がり値を演算結果に加算する。
// →演算対象は、PIDおよびすべてのデータバイト
// →LIN2.xのフレームID 0~59(0x3B)に使用
//-------------------------------------------------------------------
uint8_t CalcCheckSumEx(uint8_t pid, uint8_t dlc, uint8_t *data)
{
uint32_t Sum = PidTable[pid];
for(int i = 0; i < dlc; i++){
Sum += data[i];
if(0x000000FF < Sum){
Sum = (Sum & 0x000000FF) + 1;
}
}
return (uint8_t)(~Sum & 0x000000FF);
}
送信を開始させ、TXエンプティー割り込みが発生するとUART4_IRQHandler()がコールされます。
下記のように1バイト目の送信前にLL_USART_RequestBreakSending()をコールし、Breakフィールドを生成します。
全てのデータを送信した後、TXエンプティー割り込み無効にして、受信動作を再度有効にします。
// stm32f2xx_it.c
extern uint8_t TxBuf[11]; // 送信バッファ
extern int TxLen; // 送信するバイト数
extern int TxIdx; // 送信済みバイト数
/**
* @brief This function handles UART4 global interrupt.
*/
void UART4_IRQHandler(void)
{
/* USER CODE BEGIN UART4_IRQn 0 */
// TXエンプティー割込み発生?
if(LL_USART_IsActiveFlag_TXE(UART4)){
// 送信データあり
if(TxIdx < TxLen){
// 1バイト目送信前にBreakフィールド送信
if(TxIdx == 0){
LL_USART_RequestBreakSending(UART4);
}
// データ送信
LL_USART_TransmitData8(UART4, LinTxBuf.data[TxIdx++]);
// 送信完了
}else{
// TX Empty割り込み無効
LL_USART_DisableIT_TXE(UART4);
// 受信有効
LL_USART_EnableDirectionRx(UART4);
TxLen = 0;
TxIdx = 0;
}
}
/* USER CODE END UART4_IRQn 0 */
/* USER CODE BEGIN UART4_IRQn 1 */
/* USER CODE END UART4_IRQn 1 */
}
LIN受信
LIN受信はRXノットエンプティー割込みを使用しました。(受信レジスタにデータが格納されると発生)
LIN送信の際に生成したUART4 global interrupt()内に割込み処理を記述します。
下記サンプルコードのようにRxBuf[11]、RxLenという2個のグローバル変数を用意します。
RxBuf[11]は受信データを格納します。RxLenは受信したバイト数です。
LIN ブレーク検出割り込みでRxLenを0にして、RXノットエンプティー割り込みでRxBufにデータを格納します。
注意点としてBreakフィールド受信でフレーミングエラーが発生します。
フレーミングエラー発生時も、RXノットエンプティー割込みが発生するので、下記サンプルコードのようにフレーミングエラー発生時は受信データレジスタを空読みしてやる必要があります。
下記サンプルコードはデータフレームはMAXの8バイトである前提なので、その他3バイト(Synchフィールド、PIDフィールド、チェックサム)の合計11バイト受信した時点で、受信完了としています。
実際はフレームIDによってデータフレームのバイト数(0~8)が変わってくるので、受信完了のバイト数はフレームIDによって場合分けが必要です。
// stm32f2xx_it.c
uint8_t RxBuf[11]; // 受信バッファ
int RxLen; // 受信したバイト数
/**
* @brief This function handles UART4 global interrupt.
*/
void UART4_IRQHandler(void)
{
/* USER CODE BEGIN UART4_IRQn 0 */
uint8_t Data;
// LIN ブレーク検出
if(LL_USART_IsActiveFlag_LBD(UART4))
{
LL_USART_ClearFlag_LBD(UART4);
RxLen = 0;
}
// フレーミング・エラー、オーバ・ラン・エラー、 ノイズ・エラー検出
else if(LL_USART_IsActiveFlag_FE(UART4) ||
LL_USART_IsActiveFlag_ORE(UART4) ||
LL_USART_IsActiveFlag_NE(UART4))
{
Data = LL_USART_ReceiveData8(UART4);
}
// RXノットエンプティー
else if(LL_USART_IsActiveFlag_RXNE(UART4))
{
RxBuf[RxLen++] = LL_USART_ReceiveData8(UART4);
if(11 <= RxLen){
if(RxBuf[0] == 0x55){ // Synchフィールド
uint8_t frameId = RxBuf[1] & 0x3F; // フレームId
// パリティチェック
if(RxBuf[1] == PidTable[frameId]){
// チェックサム
if(CalcCheckSum(8, &RxBuf[2]) == RxBuf[10]){
// 正常受信
}else{
// チェックサムエラー
}
}else{
// パリティエラー
}
}else{
// Synchフィールドでない
}
RxLen = 0;
}
}
/* USER CODE END UART4_IRQn 0 */
/* USER CODE BEGIN UART4_IRQn 1 */
/* USER CODE END UART4_IRQn 1 */
}
下記を実行して割込みを有効にすることで上記割込みが発生します。
LL_USART_EnableIT_RXNE(UART4); // 受信割込み有効 LL_USART_EnableIT_LBD(UART4); // ブレーク検出割込み有効 LL_USART_EnableIT_ERROR(UART4);// エラー割込み有効
あとがき
自社製品 (CanLine-1)の製作にあたり、今回はじめて STM32 マイコンの LIN モード を使用してみました。
実際に触ってみて分かったことは、STM32 の LIN 機能は
LIN 規格のすべてをハードウェアで処理してくれるわけではない、という点です。
STM32 がハードウェアで対応しているのは、
-
Break フィールドの生成
-
Break フィールドの検出
までであり、
-
PID フィールドのパリティ計算
-
チェックサムの計算および検証
-
フレーム全体の制御
といった部分については、ソフトウェアで自作する必要があります。
一見すると「中途半端」にも感じられる構成ですが、個人的にはそれでも非常に助かりました。
というのも、過去に別のマイコンで Break フィールドをすべてソフトウェア実装した経験があり、その際は
-
Break 長の微調整が難しい
-
ビット長やタイミングが安定しない
-
波形が仕様ギリギリになってしまう
など、想像以上に手間と時間がかかりました。
その経験と比べると、STM32 に Break フィールドの生成機能と検出機能が用意されているだけでも非常に有難いと感じます。
LIN 通信で最もシビアになりがちな部分をハードウェアに任せられるため、上位のプロトコル処理に集中できる点は大きなメリットです。
結果として、STM32 の LIN モードは
「すべてお任せ」ではないものの、実用上とてもバランスの取れた実装だと感じました。






