This project uses the timers of a TI Hercules microcontroller to generate complex digital signals and to measure digital signals.


In this post: create a triangle wave by modulating the duty cycle of a fixed frequency pulse wave.


triangle wave from NHET2 timer


This project is based on TI's application note spna178: Monitoring PWM Using N2HET.

That note explains the functionality and has a link to a Code Composer Studio project .

I'm using these as the source for the blog series.


Triangle signal from a pulse wave


Creating a triangle wave by playing with the duty cycle of a pulse wave is a nice simple case.

We're trying to generate an analog signal purely by controlling the amount of energy in each cycle of the pulse wave.

This is different than ladder DACs that generate a level based on a digital value. Our example creates a level by controlling how long a pulse stays high vs low.

When a 3.3 V pulse has 100% duty cycle, the average delivered is 3.3 V.

When that same signal has a duty cycle of 50%, average = 1.65 V, and so forth down to 0 V at 0% duty cycle.

The output signal doesn't look like a triangle when you probe it, because it's a pulse train. When you run it through a low-pass filter, it's integrated into its analog representation: a triangle wave.

I didn't find a good picture with a triangle wave. The below one shows the very same concept but with a sinus signal generated with duty cycle modulation.

On popular demand I'll probe the actual pulse train together with the triangle wave on my launchpad.


source: part of a diagram from TI application note SPNA217: Sine Wave Generation Using PWM With Hercules™ N2HET


TI has application notes solely focused on generating different wave types (trapezoid, triangle in one and in another note sine wave) by playing with the duty cycle of the HET modules.

They do a much better attempt to explain it than I do here. Check the app notes if the above doesn't make sense.


The HET Code


The flow that generates the pulse train is shown in thie chart below:

source: TI's application note spna178: Monitoring PWM Using N2HET, showing the triangle waveform logic


Now the HET code that implements this. I believe this is among the most difficult code to read (together with TI' PRU instructions).

I need the comments to recognise the flowchart parts above.


; This example code is to be loaded to N2HET1 to generate a triangle waveform using varying
; duty cycle ramping from 0% duty to a programmable maximum duty cycle and then ramp down 
; to 0% again.

; PWM frequency to be generated
PWM_PERIOD             .equ 3

; The pin number that will output the PWM signal
PWM_PIN_NUM            .equ 9

; The initial maximum duty cycle (compare value) to be generated.
INIT_COMPARE           .equ 1

; delay amount to increment from one duty cycle to the next duty cycle.
INCREMEMT_DELAY        .equ 0

; delay amount to decrement from one duty cycle to the next duty cycle.
DECREMEMT_DELAY        .equ 0

; amount of increment 
DUTY_INCREMEMT         .equ 0

; amount of decrement
DUTY_DECREMEMT         .equ 0

; amount of increment 

; amount of decrement

; key to unlock N2HET 
UNLOCK_KEY             .equ 0xA

; The data field of the MOV32 instruction contains an initial value (0x5) that 
; is not equal to the key to unlock the N2HET program. First the MOV32 
; instruction moves the initial value toa temporaray register T
L00   MOV32 { remote=DUMMY,type=IMTOREG,reg=T,data=0x5};

; Compare the register T value with the key to unlock N2HET. The key to unlock 
; is 0xA. If the key is not matched then go back to L00. The CPU is supposed 
; to write the proper key (0xA) to unlock the N2HET
L01   ECMP { next=L00,hr_lr=LOW,cond_addr=L02,pin=0,reg=T,data=UNLOCK_KEY};

; Creating a virtual counter using CNT which will determines the period of 
; the PWM to be generated. The initial small max count allows for quick
; simulation which can later be changed by the host CPU. 
L02   CNT { reg=A,irq=ON,max=PWM_PERIOD};

; Use ECMP to determine the duty cycle of the PWM on the specified pin. The
; pin field and the duty cycle are changeable by the CPU.
L03   ECMP { hr_lr=HIGH,en_pin_action=ON,cond_addr=L04,pin=PWM_PIN_NUM,action=PULSELO,

; Only when the CNT reaches the max count will the program go to the 
; conditional address. We want to wait for one complete PWM waveform to be 
; generated before changing the duty cycle. When CNT reaches the max
; value it will set the Z flag.
L04   BR { next=L00,cond_addr=L05,event=Z};

; the data field in this ADD acts as a up/down flag. We want to create a 
; triangle waveform. The PWM will first increase the duty cycle until it 
; reaches the specified maximum duty cycle before it starts to decrease the 
; duty. The up/down flag is used to create two different paths in the flow
; to alternate before increasing duty cycle vs decreasing duty cycle. 
L05   ADD { src1=ZERO,src2=ZERO,dest=NONE,data=0};

; Move the up/down flag to a temp register T.
L06   MOV32 { remote=L05,type=REMTOREG,reg=T};

; Compare this up/down flag to 0. 0 means to increase the duty cycle and 1 
; means to decrease the duty cycle. 
L07   ECMP { next=L16,cond_addr=L08,pin=0,reg=T,data=0};

; move the ECMP DF which contains the compare value for duty cycle creation
; to register R 
L08   MOV32 { remote=L03,type=REMTOREG,reg=R};

; Subtract the current compare value from the max duty cycle stored in 
; REM_DUTY. The result will be stored in register S.
L09   SUB { src1=REM,src2=R,dest=S,remote=REM_DUTY,data=0};

; If the substraction result is more than 0 then it means it has not 
; reached the max duty cycle we will increase the duty cycle. If it is 
; zero then we have reached the max duty cycle and we will change to 
; up/down flag to down position.
L10   BR { next=L14,cond_addr=L11,event=GT};

; Insert delay before changing to the next duty cycle
L11   DJZ { next=L00,cond_addr=L12,reg=NONE,data=INCREMEMT_DELAY};

; Add specified amount to the existing compare value (duty cycle). This 
; value is also changeable by CPU
L12   ADD { src1=R,src2=IMM,dest=S,rdest=REM,remote=L03,data=DUTY_INCREMEMT,

; Reset the increment delay to the specified amount.
L13   MOV32 { next=L15,remote=L11,type=IMTOREG&REM,reg=NONE,data=INCREMEMT_DELAY};

; Now change the up/down flag to down by moving a 1 to the up/down flag
L14   MOV32 { remote=L05,type=IMTOREG&REM,reg=NONE,data=1};

; Branch to the beginning
L15   BR { next=L00,cond_addr=L00,event=NOCOND};

; move the ECMP DF to register R which contains the current compare value
; (duty cycle)
L16   MOV32 { remote=L03,type=REMTOREG,reg=R}; 

; Subtract the current duty cycle by the specified amount. This value is 
; also changeable by CPU.
L17   SUB { src1=R,src2=IMM,dest=S,rdest=NONE,data=DUTY_DECREMEMT,

; As long as the subtraction result is greater than zero, we will keep 
; decreasing the duty cyle or otherwise we will again change the up/down 
; flag to up position. The destination register is A which contains the 
; subtraction result.
L18   BR { next=L19,cond_addr=L22,event=N};

; Insert the delay before decreasing to the next duty cycle.
L19   DJZ { next=L00,cond_addr=L20,reg=NONE,data=DECREMEMT_DELAY};

; Move the subtraction result to the ECMP DF as the new duty cycle
L20   MOV32 { next=L21,remote=L03,type=REGTOREM,reg=S};

; Reset the decrement delay to the specified amount
L21   MOV32 { next=L00,remote=L19,type=IMTOREG&REM,reg=NONE,data=DECREMEMT_DELAY};

; Move the value 0 to the up/down flag so in the next LRP the program 
; flow will execute the path to increase duty cycle.
L22   MOV32 { remote=L05,type=IMTOREG&REM,reg=NONE,data=0};

; Branch to beginning
L23   BR { next=L00,cond_addr=L00,event=NOCOND};

; REM_DUTY data field stores the maximum duty cycle the PWM to be generated. 
; The host CPU can change this value.
REM_DUTY   ECMP { next=REM_DUTY,cond_addr=REM_DUTY,pin=0,reg=A,data=INIT_COMPARE,hr_data=0};
DUMMY   BR { next=DUMMY,cond_addr=DUMMY,event=NOCOND,irq=OFF};


Format is label-command-attributes. All instructions are documented in the TMS reference manual, section 17.5.

The defines in the code are placeholders in this example. All of these can be set from the firmware before the timer is kicked off ...



Give data to the HET controller at runtime from Firmware


Here's an example on how these values are overruled at runtime:


/* Change to desired PWM Period in terms of (uS)
 * the default 45.52uS will generate a 12-bit resolution on the PWM
 * with each resolution equal to HR=VCLK2=11.11nS.
 * 45.52uS / 11.11ns = 4096.
#define PWM_PERIOD    45.52F

/* Change to desired maximum duty in terms of (%)
 * N2HET1 will generate a varying PWM from 0% duty to
 * the maximum duty specified by PWM_DUTY */
#define PWM_DUTY      100.0

/* allowable LR Prescaler values are 5, 6 and 7. Anything less will
 * will not have enough time slots for the N2HET program. HR prescaler
 * is always divide by 1 from VCLK2.
 * 7 -> one LR = 128 HR
 * 6 -> one LR = 64 HR
 * 5 -> one LR = 32 HR
#define LRPFC 7

/* The NH2ET1 program will automatically increase the PWM
 * modulation from 0% duty cycle to maximum duty cycle
 * specified in PWM_DUTY. When PWM_DUTY is reached it starts
 * to decrease the duty cycle from PWM_DUTY to 0%.
 * DUTY_INCREMENT specifies the delta amount of duty cycle to
 * change from one duty cycle to the next duty cycle while
 * the duty cycle is increasing. This is expressed in terms
 * of (%). For example specifying DUTY_INCREMENT equal to
 * 2 will mean the duty cycle will start at 0% and the next
 * duty cycle will be 2% at a 2% increment. If 0 is
 * specified, then the N2HET1 will increment the duty cycle
 * at 1 HR (High Resolution) clock  */
#define DUTY_INCREMENT   0.0F

/* DUTY_INCREMEMT specifies the delta amount of duty cycle to
 * change from one duty cycle to the next duty cycle while
 * the duty cycle is decreasing. This is expressed in terms
 * of (%). */
#define DUTY_DECREMENT   0.0F

/* The increment delay is the delay at which the PWM modulation
 * will increase the duty cycle from one duty cycle to the
 * next duty cycle. This is expressed in terms
 * of (uS).  For example, if INCREMENT_DELAY is specified for
 * 10.0F (equal to 10uS) then the N2HET1 will wait for 10uS
 * before changing to the new duty cycle */

/* Decrement delay. The Decrement delay is the delay at
 * which the PWM will decrease the duty cycle. This is expressed
 * in terms of (uS).  */

/* Pin number in N2HET1 to generate the PWM. */

// ...

  /* Set N2HET1[9] to output */
  hetREG1->DIR = 1 << NHET1_PIN_PWM;

  /* Change the LRPFC according to user input */
  hetREG1->PFR = (LRPFC << 8) ;

  /* Initiailize the PWM period and duty cycle based on the defined parameters */
  hetRAM1->Instruction[pHET_L02_0].Control = (uint32)CNT_MAX_PERIOD | (hetRAM1->Instruction[pHET_L02_0].Control & 0xFD0000);
  hetRAM1->Instruction[pHET_REM_DUTY_0].Data = ecmp_compare_value;

  /* Configure the N2HET1 pin to output the PWM */
  hetRAM1->Instruction[pHET_L03_0].Control = (hetRAM1->Instruction[pHET_L03_0].Control & 0xFFFFE0FF) |
                                    (NHET1_PIN_PWM << 8);
  hetRAM1->Instruction[pHET_L12_0].Control = (hetRAM1->Instruction[pHET_L12_0].Control & 0xFFFFE0FF) |
                                    (NHET1_PIN_PWM << 8);

  /* Configure the amount of delay before increasing to the next duty cycle */
  hetRAM1->Instruction[pHET_L11_0].Data = ((uint32)RAMPUP_WAIT << 7);
  hetRAM1->Instruction[pHET_L13_0].Data = ((uint32)RAMPUP_WAIT << 7);

  /* Configure the amount of delay before decreasing to the next duty cycle */
  hetRAM1->Instruction[pHET_L19_0].Data = ((uint32)RAMPDOWN_WAIT << 7);
  hetRAM1->Instruction[pHET_L21_0].Data = ((uint32)RAMPDOWN_WAIT << 7);

  /* Duty cycle increment amount */
  if ( (uint32)DELTA_INCREMENT == 0){
    hetRAM1->Instruction[pHET_L12_0].Data = (1 << (7 - LRPFC));
  } else {
    hetRAM1->Instruction[pHET_L12_0].Data = (((uint32)DELTA_INCREMENT) << (7 - LRPFC));

  /* Duty cycle decrement amount */
  if ((uint32)DELTA_DECREMENT == 0) {
    hetRAM1->Instruction[pHET_L17_0].Data = (1 << (7 - LRPFC));
  } else {
    hetRAM1->Instruction[pHET_L17_0].Data = (((uint32)DELTA_DECREMENT) << (7 - LRPFC));

  /* Unlock the N2HET program. Initially after reset the N2HET program is locked */
  hetRAM1->Instruction[pHET_L00_0].Data = UNLOCK_KEY << 7;


The way of working is basic. The HET assembler translates your HET program in an array with binary data. One entry per instruction.

But it helps you a little with a custom  that fits your code and allows you to see where the parameters are for each instruction, so that you don't have to perform magic logic.


The assembler instruction (as discussed in the previous post , this command is entered in the projectprebuild steps:

${HET_COMPILER} -v2 -n0  -hc32 "..\HET_code\Async_PWM_Triangle_Wave.het" 


The easiest way to get your head around this is by using the flow chart and HET code side by side and trying to follow the logic.

This is doable. Much more difficult (to me) is writing a significant HET program from scratch. That's a challenge I'm going to battle this year.


Related Blog
1: adapt TI example to a LaunchPad
2: Generate Dynamic Duty Cycle
3: Measure a PWM Signal and HET Interrupts
4: Getting Started
5: Getting Started with the Wave Form Simulator