Trong bài này sẽ giới thiệu về các port song song của vi điều khiển, cách lập trình thiết lập và sử dụng memory-map I/O, các cách để truy xuất các thanh ghi I/O. Chúng ta sẽ thử từng bước trong mô phỏng, sau đó sẽ thực hiện trên hệ thống thật sử dụng logic analyzer.

Giới thiệu

Mục tiêu

  • Hiểu về parallel port (cổng song song)
  • Hiểu được cách để biến một pin thành input hoặc output thông qua direction register
  • Hiểu được các bước để init parallel port
  • Hiểu cách để truy xuất I/O register
  • Biết cách đọc dữ liệu từ input
  • Biết cách ghi dữ liệu ra output.
  • Biết cách sử dụng logic analyzer trong mô phỏng.

Trong bài này chúng ta sẽ tìm cách cấu hình I/O một chân nào đó của chip TM4C123 (MCU trong kit TIVA) chẳng hạn như cấu hình chân PA1 và PA0

I/O Port

Với các I/O port thông dụng nhất ta thường sử dụng là parallel port hay còn gọi là GPIO, cho phép chúng ta có thể trao đổi thông tin số với thế giới bên ngoài. Từ khi bắt đầu mỗi dự án cụ thể thì chúng ta đều phải xem xét hệ thống làm việc thế nào ? Làm sao để có thể test được hệ thống hoạt động. Mỗi công cụ gỡ rối(debug) hiệu quả sẽ được thiết kế để trở thành một phần của hệ thống thay vì gắn vào hệ thống sau khi được xây dựng, nghĩa là thế nào ? Khi bạn xây dựng một hệ thống thì bạn phải tính toán được cách để debug khi hệ thống của bạn có sự cố, chứ không thể chơi kiểu “mất bò mới lo làm chuồng” được 😀

Để sử dụng được I/O thì chúng ta cần phải cấu hình nó, tuy nhiên với mỗi chân của VDK sẽ có những chức năng khác nhau, ví dụ chân PA1 và PA0 vủa TM4C123 có thể vừa là chân I/O vừa là chân UART nên khi cấu hình với từng chức năng cũng sẽ là khác nhau.

JTAG(Joint Test Action Group) được chuẩn hóa với tiêu chuẩn IEE1149.1 là một tiêu chuẩn dùng để lập trình và debug vi điều khiển, với mỗi vi điều khiển ARM hầu như đều sẽ mất 5 chân cho anh JTAG này 🙁

Với I/O pin của TIVA TM4C thì có thể cấu hình được rất nhiều chức năng như

Chức năng Mô tả
UART Universal asynchronous receiver/transmitter
SSI Synchronous serial interface
I2C Inter-integrated circuit
Timer Periodic interrupts, input capture, và output compare
PWM Pulse width modulation
ADC Chyển đổi tương tự số, đo lường các tín hiệu analog
Analog Comparator Compare two analog signals
QEI Quadrature encoder interface
USB Universal serial bus
Ethernet High-speed network
CAN Controller area network

UART sẽ được sử dụng cho kết nối serial giữa 2 computer, nó là bất đồng bộ (asynchronous) và cho phép truyền thông liên tục theo cả 2 hướng

SSI là tên gọi khác của SPI, nó được dùng để giao tiếp với các thiết bị có I/O với tốc độ trung bình, trong các bài tiếp theo chúng ta sẽ dùng nó để giao tiếp với màn hình LCD. Ngoài ra có thể dùng SSI để giao tiếp với bộ chuyển đổi số tương tự(DAC) hoặc thẻ nhớ (SDC)

I2C là bus I/O đơn giản đợc dùng để giao tiếp với các thiết bị ngoại vi tốc độ thấp

Input capture và output compare sẽ được sử dụng để tạo ta ngắt định kỳ và đo lường thời gian, độ rộng xung, pha, tần số

PWM output sẽ được dùng để áp dụng điều khiển tốc độ động cơ. Với phương pháp điều khiển động cơ thông thường thì sẽ dùng input capture để đo tốc độ vòng vay và PWM để điều khiển tốc độ. Với PWM ta còn có thể tạo ra DAC nữa

ADC thường được dùng để đo biên độ của các tín hiệu analog, nó rất quan trọng trong các hẹ thống thu thập dữ liệu

Analog comparator là bộ so sánh tương tự có 2 đầu vào là analog và tạo ra đầu ra số dựa vào đầu vào tương tự lớn hơn

QEI có thể được sử dụng để giao tiếp với các động cơ brushless

USB là kết nối serial tốc độ cao

Ethernet port có thể giúp vi điều khiển có thể kết nối tới internet hoặc mạng local

CAN có thể tạo ra kết nối tốc độ cao giữa các vi điều khiển, đặc biệt nó được sử dụng nhiều trong lĩnh vực automotive và các hệ thống điều khiển phân tán.

I/O của TIVA

Sơ đồ khối TM4C123

Hình dưới đây là cấu trúc và các chức năng có thể có của các pin của LM4F120H5QR vàTM4C123GH6PM

Các pin của VDK họ LM4F/TM4C đều được chỉ định tới 8 chức năng I/O khác nhau. Các chân này có thể được cấu hình với I/O dạng số, dạng input analog, timer I/O hoặc serial I/O.

Ví dụ: PA0 có thể là I/O dạng số hoặc serial input, ở đây có 2 bus được dùng cho I/O, Digital I/O port sẽ được kết nối với cả advanced peripheral bus và advanced high performance bus. Vì là nhiều bus nên VDK có thể thực hiện các chu kỳ bus I/O liên tục với instruction lấy từ flash ROM.

LM4F120H5QR có 8 công UART,4 SSI, 4 I2C, 2 x 12 bit ADC, 12 timer, 1 CAN, 1 USB, trong khi TM4C123GH6PM  sẽ có thêm 16 PWM output,43 đường line I/O và 12 ADC input, mỗi ADC có thể chuyển đổi 1M mẫu trên giây.

Tổng quan port

Với input port thì bit direction trong thanh ghi sẽ được set là 0, nghĩa là có thể đọc được giá trị từ các chân input

Còn output port thì ngược lại, bit direction trong thanh ghi sẽ được set là 1

Hình minh họa dưới đây sẽ giúp bạn hiểu hơn khi ta cấu hình thanh ghi direction

Chân của TM4C123

Hình trên là các chức năng có thể có của mỗi chân của Launchpad, ví dụ chân PA0thì ta có thể cấu hình được nó là chân UART(U0Rx) hoặc cấu hình nó là chân CAN(CAN1Rx)

Vậy để cấu hình được những chân này thì chúng ta cần phải quan tâm tới bit configuration trong thanh ghi GPIOAMSELvà 4 bit trong thanh ghi GPIOPCTL

Ví dụ: Nếu chúng ta muốn sử dụng UART7 trên chân PE0 và PE1 thì chúng ta phải thực hiện

  • Set bit 1,0 trong thanh ghi GPIO_PORTE_DEN_R (enable digital)
  • Clear bit 1,0 trong thanh ghi GPIO_PORTE_AMSEL_R (disable analog)
  • Set các bit PMCx trong thanh ghi GPIO_PORTE_PCTL_R lên 0001 (enable UART functionality)
  • Set bit 1,0 trong thanh ghi GPIO_PORTE_AFSEL_R (enable alternate function)

Tạm thời chúng ta sẽ biết cách sơ bộ để có thể cấu hình UART7 là như thế, cụ thể trong code thế nào mình sẽ nói ở phần dưới

Với hình trên có một số điểm chúng ta cần phải lưu ý:

  • Chỉ có 43 chân
  • Tất cả các pin đều có thể làm GPIO (general purpose input/output)
  • Các pin đều có thể là GPIO hoặc là một chân có chức năng nào đó (cái này tùy vào nhà sx chip)
  • Một số chân có chức năng quen thuộc ví dụ như SSI,UART,ADC, chúng ta sẽ có thể nghiên cứu được, tuy nhiên lại có một số chức năng khác như CAN, IDX thì sẽ ít được nói đến (muốn biết thì chỉ có xem datasheet để hiểu thêm)
  • Chân PC3-PC0 không có trong bảng bởi vì 4 chân này được dành cho JTAG debugger

Như hình trên ta có thể thấy được cấu tạo cơ bản của kit launchpad TIVAC, trên board sẽ có tích hợp luôn phần debug là ICDI(Integrated In-Circuit Debug Interface) nên tha hồ mà debug khi gặp sự cố nha các bạn

Lưu ý: thường mới mua thì chỗ power selection sẽ được gạt sang PWR, để có thể nạp chương trình thì bạn phải gạt về debug

Khái niệm Input và Output Port

Input và Output Port

Chúng ta sẽ quay lại một chút ở phần input port, nó cho phép phần mềm có thể đọc từ các tín hiệu số bên ngoài, tất nhiên là chỉ đọc. Điều này có nghĩa là chu kỳ đọc từ các địa chỉ của port  sẽ trả về giá trị trên input vào thời điểm đó. Thông thường với tristate driver (hình tam giác) sẽ truyền tín hiệu đầu vào lên bus dữ liệu trong suốt một chu kỳ đọc từ địa chỉ của port. Một chu kỳ ghi vào input port thường sẽ không gây ra ảnh hưởng nhiều. Gía trị số tồn tại trên input pin được copy vào vi điều khiển khi phần mềm thực hiện đọc từ địa chỉ của port.

Với dòng LM4F/TM4C thì sẽ có digital input là 5V-tolerant, nghĩa là điện áp ở mức high là từ 2.0 tới 5.0V, với một số dòng khác như STM32F thì một số digital input là 5V tolerant, còn một số thì không

Khác với input port thì output port có thể tham gia vào chu trình đọc và ghi giống như với bộ nhớ thông thường. Hình bên dưới sẽ là readable output port (port output có thể đọc được). Một chu kỳ tới port address sẽ ảnh hưởng tới giá trị của output pin. Đặc biệt vi điều khiển sẽ đưa thông tin lên data bus và thông tin này sẽ có xung nhịp trong D fliplop. Vì nó là readable output nên chu kỳ đọc có thể truy cập vào port address và trả về giá trị hiện tại tồn tại trên port pin. Không có chỉ output trên dòng LM4F/TM4C

Bidirectional I/O Pin

Để cho các vi điều khiển có thể dễ bán hơn thì các nhà SX đã hô biến cho port có thể có 2 chức năng là input hoặc output. Với việc sử dụng thanh ghi direction xác định pin đó là input (bit direction là 0) hay là output(bit direction là 1) như hình bên dưới.

Chúng ta có thể định nghĩa một nghi thức khởi tạo như một khi chương trình được thực thi trong quá trình khởi tạo phần cứng và phần mềm. Nếu nghi thức này đưa bit direction về 0 nghĩa là port sẽ đóng vai trò là input, và ngược lại thì nó sẽ là output port

Lưu ý: rất nhiều chương trình có thể bị lỗi mà nguyên nhân bắt nguồn từ việc cấu hình các cổng I/O, ví dụ như bạn không thể write vào input port, cũng như đôi lúc không thể read được từ output port

Lập trình IO và thanh ghi

Trước khi bắt đầu chúng ta xem thử qua chương trình dưới đây, với một số bạn quen rồi thì chắc không vấn đề, chứ nếu bạn là người mới thì nhìn vào cái này đúng là như mớ bòng bong, thôi không sao cả, cứ xem qua coi bạn có thể hiểu được tới đâu

// 0.Documentation Section 
// main.c
// Runs on LM4F120 or TM4C123
// C6_InputOutput, Input from PF4, output to PF2 (blue LED)
// Authors: Daniel Valvano, Jonathan Valvano and Ramesh Yerraballi
// Date: July 8, 2013

// LaunchPad built-in hardware
// SW1 left switch is negative logic PF4 on the Launchpad
// SW2 right switch is negative logic PF0 on the Launchpad
// red LED connected to PF1 on the Launchpad
// blue LED connected to PF2 on the Launchpad
// green LED connected to PF3 on the Launchpad

// 1. Pre-processor Directives Section
// Constant declarations to access port registers using 
// symbolic names instead of addresses
#define GPIO_PORTF_DATA_R       (*((volatile unsigned long *)0x400253FC))
#define GPIO_PORTF_DIR_R        (*((volatile unsigned long *)0x40025400))
#define GPIO_PORTF_AFSEL_R      (*((volatile unsigned long *)0x40025420))
#define GPIO_PORTF_PUR_R        (*((volatile unsigned long *)0x40025510))
#define GPIO_PORTF_DEN_R        (*((volatile unsigned long *)0x4002551C))
#define GPIO_PORTF_LOCK_R       (*((volatile unsigned long *)0x40025520))
#define GPIO_PORTF_CR_R         (*((volatile unsigned long *)0x40025524))
#define GPIO_PORTF_AMSEL_R      (*((volatile unsigned long *)0x40025528))
#define GPIO_PORTF_PCTL_R       (*((volatile unsigned long *)0x4002552C))
#define SYSCTL_RCGC2_R          (*((volatile unsigned long *)0x400FE108))
// 2. Declarations Section
//   Global Variables

//   Function Prototypes
void PortF_Init(void);

// 3. Subroutines Section
// MAIN: Mandatory for a C Program to be executable
int main(void){    

  while(1){

  }
}
// Subroutine to initialize port F pins for input and output
// PF4 is input SW1 and PF2 is output Blue LED
// Inputs: None
// Outputs: None
// Notes: ...
void PortF_Init(void){ volatile unsigned long delay;
  SYSCTL_RCGC2_R |= 0x00000020;     // 1) F clock
  delay = SYSCTL_RCGC2_R;           // delay   
  GPIO_PORTF_LOCK_R = 0x4C4F434B;   // 2) unlock PortF PF0  
  GPIO_PORTF_CR_R = 0x1F;           // allow changes to PF4-0       
  GPIO_PORTF_AMSEL_R = 0x00;        // 3) disable analog function
  GPIO_PORTF_PCTL_R = 0x00000000;   // 4) GPIO clear bit PCTL  
  GPIO_PORTF_DIR_R = 0x0E;          // 5) PF4,PF0 input, PF3,PF2,PF1 output   
  GPIO_PORTF_AFSEL_R = 0x00;        // 6) no alternate function
  GPIO_PORTF_PUR_R = 0x11;          // enable pullup resistors on PF4,PF0       
  GPIO_PORTF_DEN_R = 0x1F;          // 7) enable digital pins PF4-PF0        
}
// Color    LED(s) PortF
// dark     ---    0
// red      R--    0x02
// blue     --B    0x04
// green    -G-    0x08
// yellow   RG-    0x0A
// sky blue -GB    0x0C
// white    RGB    0x0E
// pink     R-B    0x06

GPIO Register

Ok nhìn cũng chán rồi, có 1 số chỗ ví dụ GPIO_PORTF_DATA_R, GPIO_PORTF_DIR_R, GPIO_PORTF_DEN_R, chỗ này nó nằm ở đâu trong datasheet ? À datasheet là gì mà đi đâu cũng thấy các cao thủ nói rằng, chỉ cần coi datasheet là có thể làm được mọi thứ, sơ bộ nó như là 1 quyển sách mô tả về tất cả các chức năng và thành phần có trong vi điều khiển đó các bạn, để mình ví dụ minh họa cho các bạn 1 ví dụ đơn giản về cách tìm kiếm trong datasheet

Giả sử giờ với GPIO_PORTF_DATA_R, GPIO_PORTF_DIR_R, GPIO_PORTF_DEN_R, mình sẽ đi tìm trong datasheet với các từ khóa GPIO, DATA_R, DIR_R, DEN_R tương ứng với register là DATA, DIR, DEN.

Bước 1: Tải datasheet của TM4C về tại đây

Bước 2: Vào mục 10.General-Purpose Input/Outputs (GPIOs), 10.5 Register Description, ta sẽ thấy Register 1, Register 2 và Register 18 tương ứng với DATA_R, DIR_R, DEN_R

Bước 3: Vào xem register 1 và 2 chút, sau đó nhảy xuống register 18, bạn sẽ thấy bit/field 0:7 với Name là DEN, chúng ta có thể dùng nó để enable hoặc disable các chân tương ứng. Điều này nghĩa là gì ? Nếu bạn muốn dùng cái gì đó thì bạn phải khai báo, tiếp đến là enable nó lên, nếu không dùng nữa thì mình disable đi

Vậy là xong được bước ban đầu, bạn có thể tìm hiểu được 1 ít về code thông qua kí hiệu và datasheet, mình sẽ nói kỹ hơn ở các phần sau.

Với hầu hết các vi điều khiển thi hầu hết I/O port đều là memory mapped, nghĩa là với chương trình viết ra ta có thể truy xuất được tới các I/O port một cách đơn giản bằng việc đọc hoặc ghi vào các địa chỉ thích hợp. Tuy nhiên điều quan trọng mà ta phải ghi nhớ là việc thao tác vào I/O nhìn thì giống như việc đọc và ghi vào memory nhưng các I/O port thường sẽ không hoạt động như memory. Ví dụ: một số bit thì là chỉ đọc (read-only), 1 số thì là chỉ ghi(write-only), 1 số thì chỉ xóa, 1 số lại chỉ set, ngoài ra thì một số bit còn không thể điều chỉnh.

Để phần mềm có thể dễ đọc hơn thì người ta thường dùng các định nghĩa tượng trưng cho I/O port, ví dụ như việc thiết lập thanh ghi direction GPIO_PORTF_DIR_R để xác định chân nào là input và chân nào là output. Với mỗi chân thì có thể là GPIO hoặc chân chức năng khác, thì họ sẽ set bit trong thanh ghi alternate function GPIO_PORTF_AFSEL_R khi muốn dùng chức năng khác của chân (tham khảo bảng bên dưới). Sau khi thực hiện 2 thao tác với DIR_R  và AFSEL_R thì coi như đã cấu hình thành công, việc cấu hình này thường chỉ thực hiện 1 lần lúc khởi tạo, sau đó để đưa chân lên mức 1 hoặc về 1 mức không thì ta cần ghi dữ liệu vào thanh ghi Data (GPIO_PORTF_DATA_R) , ngược lại với giai đoạn cấu hình chỉ 1 lần thì chúng ta sẽ phải đọc và ghi vào thanh ghi data nhiều lần tương ứng với đầu vào và đầu ra trong chương trình.

Bảng thanh ghi

Address 7 6 5 4 3 2 1 0 Name
400F.E108 -- -- GPIOF GPIOE GPIOD GPIOC GPIOB GPIOA SYSCTL_RCGC2_R
4000.43FC DATA DATA DATA DATA DATA DATA DATA DATA GPIO_PORTA_DATA_R
4000.4400 DIR DIR DIR DIR DIR DIR DIR DIR GPIO_PORTA_DIR_R
4000.4420 SEL SEL SEL SEL SEL SEL SEL SEL GPIO_PORTA_AFSEL_R
4000.4510 PUE PUE PUE PUE PUE PUE PUE PUE GPIO_PORTA_PUR_R
4000.451C DEN DEN DEN DEN DEN DEN DEN DEN GPIO_PORTA_DEN_R
4000.4524 1 1 1 1 1 1 1 1 GPIO_PORTA_CR_R
4000.4528 0 0 0 0 0 0 0 0 GPIO_PORTA_AMSEL_R
4000.53FC DATA DATA DATA DATA DATA DATA DATA DATA GPIO_PORTB_DATA_R
4000.5400 DIR DIR DIR DIR DIR DIR DIR DIR GPIO_PORTB_DIR_R
4000.5420 SEL SEL SEL SEL SEL SEL SEL SEL GPIO_PORTB_AFSEL_R
4000.5510 PUE PUE PUE PUE PUE PUE PUE PUE GPIO_PORTB_PUR_R
4000.551C DEN DEN DEN DEN DEN DEN DEN DEN GPIO_PORTB_DEN_R
4000.5524 1 1 1 1 1 1 1 1 GPIO_PORTB_CR_R
4000.5528 0 0 AMSEL AMSEL 0 0 0 0 GPIO_PORTB_AMSEL_R
4000.63FC DATA DATA DATA DATA JTAG JTAG JTAG JTAG GPIO_PORTC_DATA_R
4000.6400 DIR DIR DIR DIR JTAG JTAG JTAG JTAG GPIO_PORTC_DIR_R
4000.6420 SEL SEL SEL SEL JTAG JTAG JTAG JTAG GPIO_PORTC_AFSEL_R
4000.6510 PUE PUE PUE PUE JTAG JTAG JTAG JTAG GPIO_PORTC_PUR_R
4000.651C DEN DEN DEN DEN JTAG JTAG JTAG JTAG GPIO_PORTC_DEN_R
4000.6524 1 1 1 1 JTAG JTAG JTAG JTAG GPIO_PORTC_CR_R
4000.6528 AMSEL AMSEL AMSEL AMSEL JTAG JTAG JTAG JTAG GPIO_PORTC_AMSEL_R
4000.73FC DATA DATA DATA DATA DATA DATA DATA DATA GPIO_PORTD_DATA_R
4000.7400 DIR DIR DIR DIR DIR DIR DIR DIR GPIO_PORTD_DIR_R
4000.7420 SEL SEL SEL SEL SEL SEL SEL SEL GPIO_PORTD_AFSEL_R
4000.7510 PUE PUE PUE PUE PUE PUE PUE PUE GPIO_PORTD_PUR_R
4000.751C DEN DEN DEN DEN DEN DEN DEN DEN GPIO_PORTD_DEN_R
4000.7524 CR 1 1 1 1 1 1 1 GPIO_PORTD_CR_R
4000.7528 0 0 AMSEL AMSEL AMSEL AMSEL AMSEL AMSEL GPIO_PORTD_AMSEL_R
4002.43FC DATA DATA DATA DATA DATA DATA GPIO_PORTE_DATA_R
4002.4400 DIR DIR DIR DIR DIR DIR GPIO_PORTE_DIR_R
4002.4420 SEL SEL SEL SEL SEL SEL GPIO_PORTE_AFSEL_R
4002.4510 PUE PUE PUE PUE PUE PUE GPIO_PORTE_PUR_R
4002.451C DEN DEN DEN DEN DEN DEN GPIO_PORTE_DEN_R
4002.4524 1 1 1 1 1 1 GPIO_PORTE_CR_R
4002.4528 AMSEL AMSEL AMSEL AMSEL AMSEL AMSEL GPIO_PORTE_AMSEL_R
4002.53FC DATA DATA DATA DATA DATA GPIO_PORTF_DATA_R
4002.5400 DIR DIR DIR DIR DIR GPIO_PORTF_DIR_R
4002.5420 SEL SEL SEL SEL SEL GPIO_PORTF_AFSEL_R
4002.5510 PUE PUE PUE PUE PUE GPIO_PORTF_PUR_R
4002.551C DEN DEN DEN DEN DEN GPIO_PORTF_DEN_R
4002.5524 1 1 1 1 CR GPIO_PORTF_CR_R
4002.5528 0 0 0 0 0 GPIO_PORTF_AMSEL_R
31-28 27-24 23-20 19-16 15-12 '11-8 '7-4 '3-0
4000.452C PMC7 PMC6 PMC5 PMC4 PMC3 PMC2 PMC1 PMC0 GPIO_PORTA_PCTL_R
4000.552C PMC7 PMC6 PMC5 PMC4 PMC3 PMC2 PMC1 PMC0 GPIO_PORTB_PCTL_R
4000.652C PMC7 PMC6 PMC5 PMC4 0x1 0x1 0x1 0x1 GPIO_PORTC_PCTL_R
4000.752C PMC7 PMC6 PMC5 PMC4 PMC3 PMC2 PMC1 PMC0 GPIO_PORTD_PCTL_R
4002.452C PMC5 PMC4 PMC3 PMC2 PMC1 PMC0 GPIO_PORTE_PCTL_R
4002.552C PMC4 PMC3 PMC2 PMC1 PMC0 GPIO_PORTF_PCTL_R
4000.6520 LOCK (write 0x4C4F434B để unlock) GPIO_PORTC_LOCK_R
4000.7520 LOCK (write 0x4C4F434B để unlock) GPIO_PORTC_LOCK_R
4000.5520 LOCK (write 0x4C4F434B để unlock) GPIO_PORTC_LOCK_R

Init port F

Thông thường để khởi tạo I/O port thì cần 7 bước

  1. Active clock cho port
  2. Unlock port, với TIVAC thì cần unlock cho PC3-0,PD7,PF0
  3. Disable analog function của pin vì chúng ở đây chúng ta dùng pin là digital I/O
  4. Clear bit PCTL để chọn regular digital function (chế độ digital thông thường)
  5. Set thanh ghi direction
  6. Clear bit trong thanh ghi alternate function
  7. Enable digital port

Lưu ý: Lỗi thường gặp nhất là bus fault nếu ta truy xuất vào port mà không enable clock

Ví dụ cụ thể luôn cho các bạn dễ hình dung, chúng ta sẽ cấu hình PF4 và PF0 là input và PF1 – PF3 là output, theo các bước ở trên thì ta phải

  1. Active clock trong thanh ghi SYSCTL_RCGC2_R để dùng port
  2. Unlock port bằng cách ghi giá trị đặc biệt vào thanh ghi LOCK, tiếp theo là thiết lập các bit trong thanh ghi CR, chỉ unlock PC3-0, PD7, và PF0 thôi nhé, mấy bit khác thì luôn unlock  rồi
  3. Disable analog functionality bằng cách clear bit trong thanh ghi AMSEL
  4. Lựa chọn GPIO functionality bằng cách clear bit trong thanh ghi PCTL
  5. Chỉnh pin là input hoặc output bằng cách xóa hoặc set các bit trong thanh ghi DIR
  6. Vì chúng ta dùng pin là digital I/O thông thường nên cần phải clear các bit trong thanh ghi AFSEL
  7. Enable các I/O pin tương ứng bằng cách write 1 lên thanh ghi DEN
  8. Để chạy được ví dụ này trên kit TIVA thì chúng ta cần phải set thêm bit trong thanh ghi PURđể dùng 2 nút nhấn có sẵn với internal pull-up resistor(điện trở nội kéo lên)
#include "tm4c123ge6pm.h"
unsigned long In;  // input from PF4

unsigned long Out; // output to PF2 (blue LED)

//   Function Prototypes

void PortF_Init(void);

// 3. Subroutines Section

// MAIN: Mandatory for a C Program to be executable

int main(void){    // initialize PF0 and PF4 and make them inputs

  PortF_Init();    // make PF3-1 out (PF3-1 built-in LEDs)

  while(1){

    In = GPIO_PORTF_DATA_R&0x10;   // read PF4 into Sw1

    In = In>>2;                    // shift into position PF2

    Out = GPIO_PORTF_DATA_R;

    Out = Out&0xFB;

    Out = Out|In;

    GPIO_PORTF_DATA_R = Out;        // output

  }

}

// Subroutine to initialize port F pins for input and output

// PF4 is input SW1 and PF2 is output Blue LED

// Inputs: None

// Outputs: None

// Notes: ...

void PortF_Init(void){ volatile unsigned long delay;

  SYSCTL_RCGC2_R |= 0x00000020;     // 1) activate clock for Port F

  delay = SYSCTL_RCGC2_R;           // allow time for clock to start

  GPIO_PORTF_LOCK_R = 0x4C4F434B;   // 2) unlock GPIO Port F

  GPIO_PORTF_CR_R = 0x1F;           // allow changes to PF4-0

  // only PF0 needs to be unlocked, other bits can't be locked

  GPIO_PORTF_AMSEL_R = 0x00;        // 3) disable analog on PF

  GPIO_PORTF_PCTL_R = 0x00000000;   // 4) PCTL GPIO on PF4-0

  GPIO_PORTF_DIR_R = 0x0E;          // 5) PF4,PF0 in, PF3-1 out

  GPIO_PORTF_AFSEL_R = 0x00;        // 6) disable alt funct on PF7-0

  GPIO_PORTF_PUR_R = 0x11;          // enable pull-up on PF0 and PF4

  GPIO_PORTF_DEN_R = 0x1F;          // 7) enable digital I/O on PF4-0

}

Khi phần mềm đọc từ địa chỉ 0x400253FC thì 8 bit cuối sẽ trả về giá trị hiện tại của PortF, 24 bit đầu sẽ trả về giá trị là 0. Như hình trên mục Bidirectional I/O Pin thì khi chúng ta đọc I/O port thì input pin sẽ show ra các trạng thái hiện tại của pin, và output pin sẽ show ra giá trị cuối cùng được ghi vào port. Hàm PortF_Input sẽ đọc giá trị của 5 chân input và trả về giá trị hiện tại của các input, trong khi đó hàm PortF_Output sẽ ghi giá trị mới vào 3 output pin.

#include sẽ định nghĩa symbolic name cho tất cả I/O port của vi điều khiển trong header file là tm4c123ge6pm.h

Friendly Code

Từ này có vẻ hơi lạ với chúng ta, nhưng nếu bạn sử dụng và thao tác trên thanh ghi nhiều thì cần phải viết code một cách thân thiện, dễ hiểu, ngoài việc chỉ thay đổi giá trị bit bạn  cần thay đổi ra thì không được làm ảnh hưởng tới các bit không liên quan. Cách điển hình để làm việc này là

  1. Đọc giá trị của I/O register lưu vào một biến
  2. Mask hoặc clear bit mà chúng ta mong muốn clear về 0 trong biến
  3. Select hoặc set bit mà chúng ta muốn set lên 1 trong biến
  4. Ghi giá trị này vào thanh ghi I/O

Ví dụ: Với portB là output, mình cần set bit 5 và clear bit 3, giải pháp tốt(gọi là friendly code) là không làm thay đổi những bit không liên quan như 7,6,4,2,1,0 thì ta phải làm như sau

GPIO_PORTB_DATA_R = (GPIO_PORTB_DATA_R&(~0x08))|0x20;

Một cách khác tương tự

in = GPIO_PORTB_DATA_R; // read value
in &= ~0x08;            // clear bit 3
in |= 0x20;             // set bit 5
GPIO_PORTB_DATA_R = in; // update port

Trong TIVAC thì portB với bit từ 23-8 sẽ không được thêm, nếu có thì với cách làm trên chúng ta cũng sẽ không làm ảnh hưởng gì tới nó cả

Địa chỉ bit đặc biệt

Một cách khác để có thể viết một chương trình gọi là friendly code là sử dụng bit-specific addressing, nghĩa là dùng địa chỉ trực tiếp của từng bit, ở trên TM4C cho phép chúng ta có thể làm điều này, tuy nhiên chỉ giới hạn ở một số thanh ghi data thôi.

Với cách trên thì ta có thể truy xuất tới bit 1 – 8 trong data port.Chúng ta có thể định nghĩa 8 địa chỉ offset như sau

Ví dụ: Chúng ta muốn truy cập vào bit 1 2 3 của port A. Với base address của Port A là 0x4000.4000 và constants là 0x0008 (bit 1),0x0010(bit 2), và 0x0020(bit 3). Tính tổng 0x4000.4000+0x0008+0x0010 +0x0020 thì địa chỉ sẽ là 0x4000.4038. Nếu chúng ta đọc từ địa chỉ này thì chỉ có giá trị của bit 1,2,3 được trả về, và tương tự nếu chúng ta write xuống địa chỉ trên thì chỉ có 3 bit được thay đổi.

Để có thể sử dụng và read/write xuống từng bit của port A thì trong chương trình ta có thể thêm một số define như sau

#define PA7   (*((volatile unsigned long *)0x40004200))
#define PA6   (*((volatile unsigned long *)0x40004100))
#define PA5   (*((volatile unsigned long *)0x40004080))
#define PA4   (*((volatile unsigned long *)0x40004040))
#define PA3   (*((volatile unsigned long *)0x40004020))
#define PA2   (*((volatile unsigned long *)0x40004010))
#define PA1   (*((volatile unsigned long *)0x40004008))
#define PA0   (*((volatile unsigned long *)0x40004004))

Giờ thì có thể đơn giản hơn để set/clear PA5 mà không ảnh hưởng tới các pin khác bằng cách

PA5 = 0x20;       // make PA5 high
PA5 = 0x00;       // make PA5 low

Nếu bạn đọc giá trị từ PA5 sẽ trả về 0x20 hoặc 0x00 tùy theo giá trị pin hiện tại là high hay low. Nếu PA5 là output thì có thể dùng đoạn code sau để thay đổi trạng thái của PA

PA5 = PA5^0x20;   // toggle PA5

Nếu bạn muốn thay đổi nhiều hơn 1 bit thì sao nhỉ ? Ví dụ luôn là thay đổi bit 6 và 7 của port A thì ta có thể define constant như sau

#define PA76   (*((volatile unsigned long *)0x40004300))
PA76 = 0x80;       // make PA7 high and PA6 low

Vậy với Port A có base address là 0x40004000 vậy thì các port khác sẽ là gì nhỉ ? Các bạn tham khảo hình bên dưới nhé

Monitor Debug với LED

Một trong những nhiệm vụ quan trọng của việc debug một hệ thống là quan sát được vị trí và thời gian mà phần mềm được thực thi. Một tool debug ngon lành là tool có thể giúp ta gỡ rối được các hệ thống realtime, trong hệ thống thời gian thực thì ta cần thời gian thực thi của tool debug là càng nhỏ càng tốt so với thời gian thực thi của chương trình. Có một khái niệm và Intrusiveness được định nghĩa là mức độ mà nó làm thay đổi hiệu suất của hệ thống đang được thử nghiệm. Ví dụ như lệnh print..

Với việc sử dụng LED thì đây cũng là một cách để debug đơn giản, người ta thường gọi là BOOLEAN monitor. Bạn có thể nối LED với 1 chân output không dùng tới, và phần mềm sẽ toggle LED để chỉ cho bạn có thể biết được là khi nào thì chương trình chạy.

#define PF2 (*((volatile unsigned long *)0x40025010))
void HeartBeat(void){
  PF2 ^= 0x04;  // toggle LED
}
#define HeartBeat2() {PF2 ^= 0x04;}

Ví dụ với chương trình trên thì hàm HeartBeat() sẽ toggle PF2, Macro HeartBeat2() cũng sẽ toggle PF2 nhưng sẽ chạy nhanh hơn bởi vì nó không có gọi hàm và return

Nếu chúng ta thêm 2 hàm HeartBeat()HeartBeat2() vao hệ thống thì sẽ mất 13 chu kỳ bus để thực thi HeartBeat và 7 chu kỳ bus để thực thi HeartBeat2. Tai bus clock là 80Mhz thì 7 chu kỳ bus sẽ là 87.5ns.

Debug với logic analyzer

Logic Analyzer và oscilloscope là 2 công cụ khá quan trọng để debug được hardware và software.

Với logic analyzer thì có thể quan sát được tín hiệu số tại nhiều thời điểm khác nhau

Với oscilloscope thì có thể quan sát được điện áp với thời gian dữ liệu

2 cách này để tới ví dụ cụ thể mình sẽ hướng dẫn các bạn luôn cho dễ hiểu

Tạm Kết

Vậy là trong bài viết này chúng ta đã có thể hiểu về parallel port (cổng song song) và cách init nó, cách để biến một pin thành input hoặc output thông qua direction register, cách để truy xuất I/O register, đọc dữ liệu từ input và ghi ra output, hiểu được cơ bản debug và cách để debug rồi, đường còn dài, cố gắng đi tiếp các bài tiếp theo nha các bạn