こんにちは、ENGかぴです。
PICマイコンに書き込まれたソースコードは読み込むことができるため開発者以外の第3者でも読み込むことができます。PICマイコンのプロテクト機能を使うとプロジェクトファイルを持たない第3者からのソースコード抜き取りが防止できます。
PICマイコンのソースコードをプロテクトの効果についてまとめています。PIC16F1827で動作確認したことについてリンクをまとめています。
PICマイコン(PIC16F1827)で実現できる機能と解説リンクまとめ
PIC16F1827のソースをプロテクト
PIC16F1827の設定はMCCを使っています。MCCの使い方については下記記事にまとめています。
MPLAB Code Configurator(MCC)の追加と使い方
今回はMCCを使用してSystem Module・EUSART・TMR0の設定を行っています。動作確認のソースコードは下記記事のものを流用しています。
EUSARTはプロテクトしたProgram memory Code内のデータを読み出して表示するために使用しています。
System Moduleの設定
最初に共通事項であるSystem Moduleの設定を行います。MPLAB X IDEを起動しMCCのアイコンをクリックしてMCCを有効にします。
内部クロックを使用するためINTOSCを選択しています。クロック周波数は任意でもよいですが4MHz_HFを選択しています。クロックを高速にするほど消費電流が増えてしまいます。
WDTのモードが4種類ありますがWDTの動作を常にONしたいためWDT_enabledを選択しています。WDT_enabledを選択するとSWDTENビットの設定は無効になります。
ソースコードのプロテクトはCONFIG1レジスタで行います。CPに「Program memory Code protection is enabled」を選択するとソースコードのプロテクトができるようになります。
CPDはEEPROMのデータのプロテクトする場合は「Data memory code protection is enabled」を選択します。
EUSARTの設定
PICマイコンのシリアル通信はEUSARTと表現されています。シリアル通信はMSSPの設定によりSPI通信やI2C通信も実現することができます。EUSARTはDevice Resources内のPeripherals欄のEUSARTを選択して追加します。
EUSARTを非同期で使用するためにModeでasynchronousを選択しEnable Transmit及びEnable Receiveにチェックを入れます。Baud Rateは19200bpsとしていますが任意でも構いません。
送信や受信時に割り込みを使用してデータの格納を行う方が確実なデータ取得がしやすいためEnable EUSART Interruptsをチェックし割り込みを使用しています。
データの取得が遅れたりするとオーバーランが発生することがあるため可能な限り受信時にレジスタからデータを獲得しておくことが推奨されます。
パリティを使用しないためTransmission Bits及びReception Bitsを8ビット構成にしています。
Software Settings欄にはTransmit Buffer SizeやReceive Buffer Sizeで割り込み時にデータを一時保存しておくバッファのサイズを選択できますがサイズを大きくするとデータ容量が増えてしまい使用できるRAMサイズを圧迫するため少なめの値にしておく方が良いでしょう。
TMR0を設定する
TMR0はDevice Resources内のPeripherals欄のTimerを選択しTMR0をクリックして追加します。
Timer Clock内でクロックに関する設定を行います。Clock Sourceは内部クロックを使用しているためFOSC(FOSC/4)を指定します。PrescalerはClock Sourceに対してTMR0のカウントアップする分周比を指定します。
Timer PeriodはTMR0がオーバーフローするタイミングを設定します。1ms毎にオーバーフロー割り込みが発生しますがCallback Function Rateを0x0A(10)を指定しているため10ms毎にコールバック関数が呼び出されます。
void mainTimer(void); //タイマー0割り込みのコールバック関数
//コールバック関数の使用例
TMR0_SetInterruptHandler(mainTimer); //コールバック関数を指定
上記例ではmainTimer()が10ms毎にコールされます。
プロテクトの動作を確認する
プロテクトの効果についてプロテクトしなかった場合とプロテクトした場合の結果を比較して動作確認を行います。またプロテクトしたProgram memory Codeのアドレスを参照して読み込むことでユーザーのソースコード内ではプロテクトされずに読み込めることを確認します。
プロテクトの効果の確認
プロテクトの効果について確認します。MPLAB X IDEを使って「Make and Program Device」でソフトを書き込みますが、プロテクトを行う場合と行わない場合の2通りのソフトを書き込んで「Read Device Memory」でProgram memory Codeを読み込みます。
プロテクトなしの場合はProgram memory Codeが読み出されています。定数で宣言したバージョン情報がAddress:0x392から「PIC16F1827-RES Ver1.00」で格納されていることが分かります。ASCIIで確認するとPICマイコンのOpcodeを含めて表示されるため0x34(文字で4)が入っていますが4を抜いて見ると文字列が確認できます。
プロテクトを使用すると「Read Device Memory」で読み出しても値がすべて0になっており内容を確認することができなくなります。プロジェクトファイルを持たない第3者からソースコードを抜き取られることはありません。
ユーザーのソースコード内では参照ができることを確認
プロテクトはプロジェクトファイル以外からProgram momory Code領域を読み出した時プロテクトして内容を分からなくするものです。ユーザーのソースコード内ではプロテクトされずに値を参照できます。
バージョン情報を文字列として定数定義するとProgram memory Code領域に格納されます。PIC16F1827のソースコードでProgram memory Code領域のアドレスを参照して電文を生成します。
SW1を押すとArudino UNOから電文を送出し受信したPIC16F1827はProgram memory Code領域の文字列を読み込んでArudinoに返信します。
SW2は電文がバージョン情報を要求する電文以外を受信するとNGを返信します。(SW2はプロテクトとは関係ありませんが動作確認のため実装しています。)ピンの設定は以下の通りです。
ユーザーのソースコード内での読み出しになるためプロテクトされずにArduinoのシリアルモニタに文字が表示されるのを確認しました。
SW1を押すとシリアルモニタに「PIC16F1827-ROM Ver1.00」が表示されています。SW2を押すとバージョン確認の電文ではないため「NG」を表示しています。
ソースコード全体
以下のソースコードはコンパイルして動作確認をしております。コメントなど細かな部分で間違っていたりやライブラリの更新などにより動作しなくなったりする可能性があります。参考としてお使いいただければと思います。
PIC16F1827のソースコード:
#include "mcc_generated_files/mcc.h"
#define TIME_OFF -1 //タイマーを使用しない場合
#define TIME_UP 0 //タイムアップ
#define TIM_RX_WAIT 20 //200ms
#define TIM_TX_WAIT 10 //100ms
#define TIM_LED_WAIT 25 //250ms
#define RING_SZ 64
#define SZ_MASK 0x3F
#define HEADER_A 'A'
#define HEADER_P 'P'
#define OFFSET_SZ 3
#define TX_SZ 7
const char ID_INFO[2][16] = {
{"PIC16F1827-ROM "},
{" Ver1.00"}
};
typedef struct{
uint8_t wp;
uint8_t rp;
uint8_t buf[RING_SZ];
}RING_MNG;
typedef struct{
uint8_t cnt;
uint8_t max;
uint8_t data[sizeof(ID_INFO)+OFFSET_SZ];
}TX_MNG;
typedef enum{ //モード
COM_RX_WAIT = 0,
COM_RCVDATA,
COM_TX,
COM_MAX
}COM_MODE_NO;
/* 変数定義 */
int8_t timRxWait = TIME_OFF;
int8_t timTxWait = TIME_OFF;
RING_MNG picRcv;
COM_MODE_NO ComMode;
uint8_t Rcvdata[RING_SZ];
TX_MNG Txdata;
/* 関数プロトタイプ宣言 */
void eUsartmain(void);
uint8_t CalcSum(uint8_t *buf, uint8_t sz);
void mainTimer(void); //タイマー0割り込みでの処理
void ReadPointerAdd(void);
bool RcvChk(void);
void TxDataSet(uint8_t no);
void mainApp(void);
/* Main application */
void main(void)
{
SYSTEM_Initialize();
TMR0_SetInterruptHandler(mainTimer);
INTERRUPT_GlobalInterruptEnable();
INTERRUPT_PeripheralInterruptEnable();
while (1)
{
eUsartmain();
mainApp();
}
}
/* タイマ管理関数 */
void mainTimer(void){
if( timRxWait > TIME_UP ){
--timRxWait; //タイマを更新
}
if( timTxWait > TIME_UP ){
--timTxWait; //タイマを更新
}
}
/* メイン関数 */
void mainApp(void){
while(EUSART_is_rx_ready()){
picRcv.buf[picRcv.wp] = EUSART_Read(); //データをリード
if( ++picRcv.wp >= sizeof(picRcv.buf)){ //次に保管する位置を更新
picRcv.wp = 0;
}
}
}
/* シリアル通信メイン処理 */
void eUsartmain(void){
int8_t rxsz;
uint8_t datsz;
uint8_t allsz;
uint8_t rp = picRcv.rp;
uint8_t i;
uint8_t sumchk;
uint8_t dat[2];
if( timRxWait == TIME_UP){ //受信タイムアウトか
timRxWait = TIME_OFF;
ReadPointerAdd();
ComMode = COM_RX_WAIT;
}
switch( ComMode ){
case COM_RX_WAIT:
rxsz = picRcv.wp - picRcv.rp; //受信データ数の算出
if( rxsz < 0 ){
rxsz = rxsz + sizeof(picRcv.buf);
}
if( rxsz == 0 ){
timRxWait = TIME_OFF;
}
else{
if( timRxWait == TIME_OFF ){
timRxWait = TIM_RX_WAIT; //受信タイムアウトをセット
}
if( picRcv.buf[ rp ] != HEADER_P ){ //ヘッダーが一致するか
ReadPointerAdd(); //読み込み位置の更新
}
else{
if( rxsz >= 2 ){ //データ長分だけ獲得しているか
for( i = 0; i < 2; i++){//データサイズ算出のため仮おき
dat[i] = picRcv.buf[ rp ];
if(++rp >= sizeof(picRcv.buf) ) rp = 0;
}
if( dat[1] > sizeof(picRcv.buf) - OFFSET_SZ ){
allsz = sizeof(picRcv.buf) - OFFSET_SZ;
}
else{
datsz = dat[1];
allsz = dat[1] + OFFSET_SZ; //header,datalengh,sumを含む
}
if( rxsz >= allsz){ //確定したサイズ以上か
timRxWait = TIME_OFF;
for(i=0; i < sizeof(Rcvdata); i++){
Rcvdata[i] = 0; //バッファのクリア
}
for( i=0; i < allsz; i++ ){
Rcvdata[i] = picRcv.buf[picRcv.rp]; //一時保管したデータを移す
ReadPointerAdd(); //読み込み位置の更新
}
sumchk = CalcSum(&Rcvdata[2], datsz); //チェックサムの計算
if( sumchk == Rcvdata[allsz-1]){ //チェックサムの確認
ComMode = COM_RCVDATA; //モードを遷移
}
}
}
}
}
break;
case COM_RCVDATA:
if( RcvChk() ){
ComMode = COM_TX;
timTxWait = TIM_TX_WAIT;
}
else{
ComMode = COM_RX_WAIT;
}
break;
case COM_TX:
if(EUSART_is_tx_ready()){ //送信ビジーでないか
if ( Txdata.cnt < Txdata.max ){
EUSART_Write(Txdata.data[Txdata.cnt]); //データを送信
++Txdata.cnt; //送信データカウントを更新
}
else{ //全データの送信が完了
timTxWait = TIME_OFF;
ComMode = COM_RX_WAIT; //受信待ちに遷移
}
}
if( timTxWait == TIME_UP ){ //送信タイムアウトか
timTxWait = TIME_OFF;
ComMode = COM_RX_WAIT; //受信待ちに遷移
}
break;
}
}
/* 読み込み位置の更新 */
void ReadPointerAdd(void){
if(++picRcv.rp >= sizeof(picRcv.buf) ){
picRcv.rp = 0;
}
}
/* チェックサムの計算 */
uint8_t CalcSum(uint8_t *buf, uint8_t sz ){
uint8_t ret = 0;
for(uint8_t i=0; i < sz; i++ ){
ret += *buf;
buf++;
}
return ret;
}
/* 電文の処理 */
bool RcvChk(void){
bool ret = false;
if( Rcvdata[2] == 0x30 && Rcvdata[3] == 0x31 &&
Rcvdata[4] == 0x32 && Rcvdata[5] == 0x33 ){
TxDataSet(0);
ret = true;
}
else{
TxDataSet(1);
ret = true;
}
return ret;
}
/* 送信データ生成 */
void TxDataSet(uint8_t no){
uint8_t i;
switch(no){
case 0:
Txdata.cnt = 0;
Txdata.max = sizeof(ID_INFO)+OFFSET_SZ;
Txdata.data[0] = HEADER_A;
Txdata.data[1] = sizeof(ID_INFO);
for(i=0; i < sizeof(ID_INFO); i++ ){
Txdata.data[ i + 2 ] = ID_INFO[0][i];
}
Txdata.data[Txdata.max-1] = CalcSum(&Txdata.data[2], Txdata.data[1]);
break;
case 1:
Txdata.cnt = 0;
Txdata.max = 5;
Txdata.data[0] = HEADER_A;
Txdata.data[1] = 2;
Txdata.data[2] = 'N';
Txdata.data[3] = 'G';
Txdata.data[4] = CalcSum(&Txdata.data[2], Txdata.data[1]);
break;
}
}
/* End of File */
本ソースコードはMPLAB X IDEにMCCのプラグインをインストールしていることが前提となります。MCCをインストールする方法は下記記事を参考にしてください。
MPLAB Code Configurator(MCC)の追加と使い方
Arduino UNOのソースコード:
#include <SoftwareSerial.h>
#define BASE_TIME 10
#define TIME_FILTER_MAX 1 //ベースタイマカウント値
#define TIM_RX_WAIT 5 //50ms
#define DI_NUM 4 //DIフィルタのサンプリング数
#define DI_MAX 2
#define TIME_OFF -1 //タイマーを使用しない場合
#define TIME_UP 0 //タイムアップ
#define PIN_DI1 7
#define PIN_DI2 8
#define RING_SZ 64
#define HEADER_A 'A'
#define HEADER_P 'P'
#define OFFSET_SZ 3
#define CNT_INIT_MAX 10 //10ms×10 = 100ms
SoftwareSerial mySerial(10, 11); // RX, TX
typedef struct{
uint8_t wp;
uint8_t buf[DI_MAX][ DI_NUM ];
uint8_t di[DI_MAX];
}FILT_DATA;
typedef struct{
uint8_t wp;
uint8_t rp;
uint8_t buf[RING_SZ];
}RING_MNG;
//--------------変数定義----------------------------
uint8_t buf[]={HEADER_P,0x04,0x30,0x31,0x32,0x33,0x00}; //電文送出パターン1
uint8_t buf2[]={HEADER_P,0x04,0x33,0x32,0x31,0x30,0x00}; //電文送出パターン2
uint32_t basemillis;
int8_t timfilter;
int8_t timRxWait = TIME_OFF;
bool btn1hold;
bool btn2hold;
FILT_DATA DiData;
RING_MNG ArudinoRcv;
uint8_t Rcvdata[RING_SZ];
uint8_t CntInit; //初期化時のみ使用
//--------------関数プロトタイプ宣言----------------------------
uint8_t CalcSum(uint8_t *buf, uint8_t sz );
void mainTimer(void);
void DiFilter(void);
void Rcvmain(void);
void ReadPointerAdd(void);
void setup() {
Serial.begin(19200);
mySerial.begin(19200);
pinMode(PIN_DI1,INPUT_PULLUP);
pinMode(PIN_DI2,INPUT_PULLUP);
DiFilterInit(); //メイン処理前にDIを確定する
}
void loop() {
mainTimer();
DiFilter();
Rcvmain();
while( Serial.available()){
ArudinoRcv.buf[ArudinoRcv.wp] = Serial.read(); //データリード
if( ++ArudinoRcv.wp >= sizeof(ArudinoRcv.buf)){
ArudinoRcv.wp = 0;
}
}
if( DiData.di[0] == 0){
if( btn1hold == false ){
btn1hold = true;
buf[6] = CalcSum(&buf[2],4); //sumの計算
mySerial.write(&buf[0],7); //データ送信
}
}
else{
btn1hold = false;
}
if( DiData.di[1] == 0){
if( btn2hold == false){
btn2hold = true;
buf2[6] = CalcSum(&buf2[2],4); //sumの計算
mySerial.write(&buf2[0],7);
}
}
else{
btn2hold = false;
}
}
/* タイマ管理 */
void mainTimer(){
if( millis() - basemillis > BASE_TIME ){
basemillis = millis();
if( timfilter > TIME_UP ){
--timfilter; //タイマを更新
}
if( timRxWait > TIME_UP ){
--timRxWait;
}
}
}
/* チェックサムの計算 */
uint8_t CalcSum(uint8_t *buf, uint8_t sz ){
uint8_t ret = 0;
for(uint8_t i=0; i < sz; i++ ){
ret += *buf;
buf++;
}
return ret;
}
/* DIフィルタの初期化 */
void DiFilterInit(void){
CntInit = CNT_INIT_MAX;
while(CntInit > 0){ //0になるまでフィルタを実施
timfilter = TIME_UP;
DiFilter(); //DIフィルタ処理
delay(10); //10ms遅延させてDIフィルタ処理
CntInit--;
}
}
/* DIフィルタ */
void DiFilter(void){
uint8_t i;
uint8_t j;
bool boo[DI_MAX]={true,true};
if( timfilter == TIME_UP ){
timfilter = TIME_FILTER_MAX;
DiData.buf[0][ DiData.wp ] = digitalRead(PIN_DI1);
DiData.buf[1][ DiData.wp ] = digitalRead(PIN_DI2);
for( i = 0; i < DI_MAX; i++ ){
for( j = 1; j < DI_NUM; j++){
if( DiData.buf[i][j-1] != DiData.buf[i][j] ){
boo[i] = false;
break;
}
}
if( boo[ i ]){ //比較して一致したら値を採用
DiData.di[i] = DiData.buf[i][0];
}
}
if( ++DiData.wp >= DI_NUM){
DiData.wp = 0;
}
}
}
/* 受信データの処理 */
void Rcvmain(void){
int8_t rxsz;
uint8_t sz;
uint8_t datsz;
uint8_t allsz;
uint8_t rp = ArudinoRcv.rp;
uint8_t i;
uint8_t sumchk;
uint8_t dat[2];
if( timRxWait == TIME_UP){
timRxWait = TIME_OFF;
ReadPointerAdd();
}
rxsz = ArudinoRcv.wp - ArudinoRcv.rp; //受信データ数の算出
if( rxsz < 0 ){
rxsz = rxsz + sizeof(ArudinoRcv.buf);
}
if( rxsz == 0 ){
timRxWait = TIME_OFF;
}
else{
if( timRxWait == TIME_OFF ){
timRxWait = TIM_RX_WAIT;
}
if( ArudinoRcv.buf[ rp ] != HEADER_A ){ //ヘッダーの確認
ReadPointerAdd();
}
else{
if( rxsz >= 2 ){
for( i = 0; i < 2; i++){//データサイズ算出のため仮おき
dat[i] = ArudinoRcv.buf[ rp ];
if(++rp >= sizeof(ArudinoRcv.buf) ) rp = 0;
}
if( dat[1] > sizeof(ArudinoRcv.buf) - OFFSET_SZ ){
allsz = sizeof(ArudinoRcv.buf) - OFFSET_SZ;
}
else{
datsz = dat[1];
allsz = dat[1] + OFFSET_SZ; //header,datalengh,sumを含む
}
if( rxsz >= allsz){
timRxWait = TIME_OFF;
for(i=0; i < sizeof(Rcvdata); i++){
Rcvdata[i] = 0;
}
for( i=0; i < allsz; i++ ){
Rcvdata[i] = ArudinoRcv.buf[ArudinoRcv.rp];
ReadPointerAdd();
}
sumchk = CalcSum(&Rcvdata[2], datsz);
if( sumchk == Rcvdata[allsz-1]){ //チェックサムが一致するか
Serial.write(&Rcvdata[2],datsz); //文字列部分をシリアルモニタに表示
Serial.println("");
}
}
}
}
}
}
/* 読み込み位置の更新 */
void ReadPointerAdd(void){
if(++ArudinoRcv.rp >= sizeof(ArudinoRcv.buf) ){
ArudinoRcv.rp = 0;
}
}
関連リンク
PICマイコンを使ってマイコンのレジスタの設定やMPLAB X IDEのプラグインであるMCCを使用して動作確認したことについてまとめています。
PICマイコン(PIC12F675)で実現できる機能と解説リンクまとめ
PICマイコン(PIC16F1827)で実現できる機能と解説リンクまとめ
最後まで、読んでいただきありがとうございました。
PICマイコンを使った製品を開発する場合は基本的にソースコードが抜き取られないようにプロテクトしておくことを意識しておくとよいと思います。